Project

General

Profile

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

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

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

    
24
import javax.persistence.EntityNotFoundException;
25

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

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

    
136

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

    
147
    private static final Logger logger = Logger.getLogger(TaxonServiceImpl.class);
148

    
149
    public static final String POTENTIAL_COMBINATION_NAMESPACE = "Potential combination";
150

    
151
    public static final String INFERRED_EPITHET_NAMESPACE = "Inferred epithet";
152

    
153
    public static final String INFERRED_GENUS_NAMESPACE = "Inferred genus";
154

    
155
    @Autowired
156
    private ITaxonNodeDao taxonNodeDao;
157

    
158
    @Autowired
159
    private ITaxonNameDao nameDao;
160

    
161
    @Autowired
162
    private INameService nameService;
163

    
164
    @Autowired
165
    private IOccurrenceService occurrenceService;
166

    
167
    @Autowired
168
    private ITaxonNodeService nodeService;
169

    
170
    @Autowired
171
    private IDescriptionService descriptionService;
172

    
173
    @Autowired
174
    private IReferenceService referenceService;
175
//
176
//    @Autowired
177
//    private IOrderedTermVocabularyDao orderedVocabularyDao;
178

    
179
    @Autowired
180
    private IOccurrenceDao occurrenceDao;
181

    
182
    @Autowired
183
    private IClassificationDao classificationDao;
184

    
185
    @Autowired
186
    private AbstractBeanInitializer beanInitializer;
187

    
188
    @Autowired
189
    private ILuceneIndexToolProvider luceneIndexToolProvider;
190

    
191
//************************ CONSTRUCTOR ****************************/
192
    public TaxonServiceImpl(){
193
        if (logger.isDebugEnabled()) { logger.debug("Load TaxonService Bean"); }
194
    }
195

    
196
// ****************************** METHODS ********************************/
197

    
198
    @Override
199
    public TaxonBase load(UUID uuid, boolean includeUnpublished, List<String> propertyPaths) {
200
        return dao.load(uuid, includeUnpublished, propertyPaths);
201
    }
202

    
203
    @Override
204
    public List<TaxonBase> searchByName(String name, boolean includeUnpublished, Reference sec) {
205
        return dao.getTaxaByName(name, includeUnpublished, sec);
206
    }
207

    
208
    @Override
209
    @Transactional(readOnly = false)
210
    public UpdateResult swapSynonymAndAcceptedTaxon(Synonym synonym, Taxon acceptedTaxon, boolean setNameInSource, boolean newUuidForAcceptedTaxon){
211
        if (newUuidForAcceptedTaxon){
212
            return swapSynonymAndAcceptedTaxonNewUuid(synonym, acceptedTaxon, setNameInSource);
213
        }else{
214
            return swapSynonymAndAcceptedTaxon(synonym, acceptedTaxon, setNameInSource);
215
        }
216
    }
217

    
218
    private UpdateResult swapSynonymAndAcceptedTaxon(Synonym synonym, Taxon acceptedTaxon, boolean setNameInSource){
219
        UpdateResult result = new UpdateResult();
220
        String oldTaxonTitleCache = acceptedTaxon.getTitleCache();
221

    
222
    	TaxonName synonymName = synonym.getName();
223
    	TaxonName taxonName = HibernateProxyHelper.deproxy(acceptedTaxon.getName());
224

    
225
    	acceptedTaxon.setName(synonymName);
226
    	synonym.setName(taxonName);
227

    
228
    	//nameUsedInSource
229
    	handleNameUsedInSourceForSwap(setNameInSource, taxonName, oldTaxonTitleCache, acceptedTaxon.getDescriptions());
230

    
231
    	saveOrUpdate(acceptedTaxon);
232
    	saveOrUpdate(synonym);
233
    	result.setCdmEntity(acceptedTaxon);
234
    	result.addUpdatedObject(synonym);
235

    
236
		return result;
237
    }
238

    
239
    private UpdateResult swapSynonymAndAcceptedTaxonNewUuid(Synonym synonym, Taxon acceptedTaxon, boolean setNameInSource){
240
        UpdateResult result = new UpdateResult();
241
        acceptedTaxon.removeSynonym(synonym);
242
        TaxonName synonymName = synonym.getName();
243
        TaxonName taxonName = HibernateProxyHelper.deproxy(acceptedTaxon.getName());
244
        String oldTaxonTitleCache = acceptedTaxon.getTitleCache();
245

    
246
        boolean sameHomotypicGroup = synonymName.getHomotypicalGroup().equals(taxonName.getHomotypicalGroup());
247
        synonymName.removeTaxonBase(synonym);
248

    
249
        List<Synonym> synonyms = new ArrayList<>();
250
        for (Synonym syn: acceptedTaxon.getSynonyms()){
251
            syn = HibernateProxyHelper.deproxy(syn, Synonym.class);
252
            synonyms.add(syn);
253
        }
254
        for (Synonym syn: synonyms){
255
            acceptedTaxon.removeSynonym(syn);
256
        }
257
        Taxon newTaxon = acceptedTaxon.clone(true, true, false, true);
258

    
259
        //move descriptions
260
        Set<TaxonDescription> descriptionsToCopy = new HashSet<>(acceptedTaxon.getDescriptions());
261
        for (TaxonDescription description: descriptionsToCopy){
262
            newTaxon.addDescription(description);
263
        }
264
        //nameUsedInSource
265
        handleNameUsedInSourceForSwap(setNameInSource, taxonName, oldTaxonTitleCache, newTaxon.getDescriptions());
266

    
267
        newTaxon.setName(synonymName);
268

    
269
        newTaxon.setPublish(synonym.isPublish());
270
        for (Synonym syn: synonyms){
271
            if (!syn.getName().equals(newTaxon.getName())){
272
                newTaxon.addSynonym(syn, syn.getType());
273
            }
274
        }
275

    
276
        //move all data to new taxon
277
        //Move Taxon RelationShips to new Taxon
278
        for(TaxonRelationship taxonRelationship : newTaxon.getTaxonRelations()){
279
            newTaxon.removeTaxonRelation(taxonRelationship);
280
        }
281

    
282
        for(TaxonRelationship taxonRelationship : acceptedTaxon.getTaxonRelations()){
283
            Taxon fromTaxon = HibernateProxyHelper.deproxy(taxonRelationship.getFromTaxon());
284
            Taxon toTaxon = HibernateProxyHelper.deproxy(taxonRelationship.getToTaxon());
285
            if (fromTaxon == acceptedTaxon){
286
                newTaxon.addTaxonRelation(taxonRelationship.getToTaxon(), taxonRelationship.getType(),
287
                        taxonRelationship.getCitation(), taxonRelationship.getCitationMicroReference());
288

    
289
            }else if(toTaxon == acceptedTaxon){
290
               fromTaxon.addTaxonRelation(newTaxon, taxonRelationship.getType(),
291
                        taxonRelationship.getCitation(), taxonRelationship.getCitationMicroReference());
292
               saveOrUpdate(fromTaxon);
293

    
294
            }else{
295
                logger.warn("Taxon is not part of its own Taxonrelationship");
296
            }
297

    
298
            // Remove old relationships
299
            fromTaxon.removeTaxonRelation(taxonRelationship);
300
            toTaxon.removeTaxonRelation(taxonRelationship);
301
            taxonRelationship.setToTaxon(null);
302
            taxonRelationship.setFromTaxon(null);
303
        }
304

    
305
        //taxon nodes
306
        List<TaxonNode> nodes = new ArrayList<>(acceptedTaxon.getTaxonNodes());
307
        for (TaxonNode node: nodes){
308
            node = HibernateProxyHelper.deproxy(node);
309
            TaxonNode parent = node.getParent();
310
            acceptedTaxon.removeTaxonNode(node);
311
            node.setTaxon(newTaxon);
312
            if (parent != null){
313
                parent.addChildNode(node, null, null);
314
            }
315
        }
316

    
317
        //synonym
318
        Synonym newSynonym = synonym.clone();
319
        newSynonym.setName(taxonName);
320
        newSynonym.setPublish(acceptedTaxon.isPublish());
321
        if (sameHomotypicGroup){
322
            newTaxon.addSynonym(newSynonym, SynonymType.HOMOTYPIC_SYNONYM_OF());
323
        }else{
324
            newTaxon.addSynonym(newSynonym, SynonymType.HETEROTYPIC_SYNONYM_OF());
325
        }
326

    
327
        //deletes
328
        TaxonDeletionConfigurator conf = new TaxonDeletionConfigurator();
329
        conf.setDeleteNameIfPossible(false);
330
        SynonymDeletionConfigurator confSyn = new SynonymDeletionConfigurator();
331
        confSyn.setDeleteNameIfPossible(false);
332
        result.setCdmEntity(newTaxon);
333

    
334
        DeleteResult deleteResult = deleteTaxon(acceptedTaxon.getUuid(), conf, null);
335
        if (synonym.isPersited()){
336
            synonym.setSecSource(null);
337
            deleteResult.includeResult(deleteSynonym(synonym.getUuid(), confSyn));
338
        }
339
        result.includeResult(deleteResult);
340

    
341
        return result;
342
    }
343

    
344
    private void handleNameUsedInSourceForSwap(boolean setNameInSource, TaxonName taxonName, String oldTaxonTitleCache,
345
            Set<TaxonDescription> descriptions) {
346
        for(TaxonDescription description : descriptions){
347
            String message = "Description copied from former accepted taxon: %s (Old title: %s)";
348
            message = String.format(message, oldTaxonTitleCache, description.getTitleCache());
349
            description.setTitleCache(message, true);
350
            if(setNameInSource){
351
                for (DescriptionElementBase element: description.getElements()){
352
                    for (DescriptionElementSource source: element.getSources()){
353
                        if (source.getNameUsedInSource() == null){
354
                            source.setNameUsedInSource(taxonName);
355
                        }
356
                    }
357
                }
358
            }
359
        }
360
    }
361

    
362
    @Override
363
    @Transactional(readOnly = false)
364
    public UpdateResult changeSynonymToAcceptedTaxon(Synonym synonym, Taxon acceptedTaxon, Reference newSecRef, String microRef, SecReferenceHandlingEnum secHandling, boolean deleteSynonym) {
365
        UpdateResult result = new UpdateResult();
366
        TaxonName acceptedName = acceptedTaxon.getName();
367
        TaxonName synonymName = synonym.getName();
368
        HomotypicalGroup synonymHomotypicGroup = synonymName.getHomotypicalGroup();
369

    
370
        //check synonym is not homotypic
371
        if (acceptedName.getHomotypicalGroup().equals(synonymHomotypicGroup)){
372
            String message = "The accepted taxon and the synonym are part of the same homotypical group and therefore can not be both accepted.";
373
            result.addException(new HomotypicalGroupChangeException(message));
374
            result.setAbort();
375
            return result;
376
        }
377
        if (secHandling != null && secHandling.equals(SecReferenceHandlingEnum.KeepAlways)){
378
            newSecRef = synonym.getSec();
379
        }
380
        Taxon newAcceptedTaxon = Taxon.NewInstance(synonymName, newSecRef, microRef);
381
        newAcceptedTaxon.setPublish(synonym.isPublish());
382
        dao.save(newAcceptedTaxon);
383
        result.setCdmEntity(newAcceptedTaxon);
384
        SynonymType relTypeForGroup = SynonymType.HOMOTYPIC_SYNONYM_OF();
385
        List<Synonym> heteroSynonyms = acceptedTaxon.getSynonymsInGroup(synonymHomotypicGroup);
386

    
387
        for (Synonym heteroSynonym : heteroSynonyms){
388
            if (secHandling == null || !secHandling.equals(SecReferenceHandlingEnum.KeepAlways)){
389
                heteroSynonym.setSec(newSecRef);
390
            }
391
            if (synonym.equals(heteroSynonym)){
392
                acceptedTaxon.removeSynonym(heteroSynonym, false);
393
            }else{
394
                //move synonyms in same homotypic group to new accepted taxon
395
                newAcceptedTaxon.addSynonym(heteroSynonym, relTypeForGroup);
396
            }
397
        }
398
        dao.saveOrUpdate(acceptedTaxon);
399
        result.addUpdatedObject(acceptedTaxon);
400
        if (deleteSynonym){
401

    
402
            try {
403
                this.dao.flush();
404
                SynonymDeletionConfigurator config = new SynonymDeletionConfigurator();
405
                config.setDeleteNameIfPossible(false);
406
                this.deleteSynonym(synonym, config);
407

    
408
            } catch (Exception e) {
409
                result.addException(e);
410
            }
411
        }
412

    
413
        return result;
414
    }
415

    
416
    @Override
417
    @Transactional(readOnly = false)
418
    public UpdateResult changeSynonymToAcceptedTaxon(UUID synonymUuid,
419
            UUID acceptedTaxonUuid,
420
            UUID newParentNodeUuid,
421
            UUID newSec,
422
            String microReference,
423
            SecReferenceHandlingEnum secHandling,
424
            boolean deleteSynonym)  {
425
        UpdateResult result = new UpdateResult();
426
        Synonym synonym = CdmBase.deproxy(dao.load(synonymUuid), Synonym.class);
427
        Taxon acceptedTaxon = CdmBase.deproxy(dao.load(acceptedTaxonUuid), Taxon.class);
428
        TaxonNode newParentNode = taxonNodeDao.load(newParentNodeUuid);
429
        Reference newSecRef = null;
430
        switch (secHandling){
431
            case AlwaysDelete:
432
                newSecRef = null;
433
                break;
434
            case KeepAlways:
435
                break;
436
            case UseNewParentSec:
437
                newSecRef = newParentNode.getTaxon() != null? newParentNode.getTaxon().getSec(): null;
438
                break;
439
            case KeepWhenSame:
440
                Reference parentSec = newParentNode.getTaxon() != null? newParentNode.getTaxon().getSec(): null;
441
                Reference synSec = synonym.getSec();
442
                if (parentSec != null && synSec != null && parentSec.equals(synSec)){
443
                    newSecRef = synonym.getSec();
444
                }else{
445
                    newSecRef = CdmBase.deproxy(referenceService.load(newSec));
446
                }
447
                break;
448
            case WarningSelect:
449
                newSecRef = CdmBase.deproxy(referenceService.load(newSec));
450
                break;
451
            default:
452
                break;
453
        }
454

    
455
        result =  changeSynonymToAcceptedTaxon(synonym, acceptedTaxon, newSecRef, microReference, secHandling, deleteSynonym);
456
        Taxon newTaxon = (Taxon)result.getCdmEntity();
457

    
458
        TaxonNode newNode = newParentNode.addChildTaxon(newTaxon, null, null);
459
        taxonNodeDao.save(newNode);
460
        result.addUpdatedObject(newTaxon);
461
        result.addUpdatedObject(acceptedTaxon);
462
        result.setCdmEntity(newNode);
463
        return result;
464
    }
465

    
466
    @Override
467
    @Transactional(readOnly = false)
468
    public UpdateResult changeSynonymToRelatedTaxon(UUID synonymUuid,
469
            UUID toTaxonUuid,
470
            TaxonRelationshipType taxonRelationshipType,
471
            Reference citation,
472
            String microcitation){
473

    
474
        UpdateResult result = new UpdateResult();
475
        Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
476
        Synonym synonym = (Synonym) dao.load(synonymUuid);
477
        result = changeSynonymToRelatedTaxon(synonym, toTaxon, taxonRelationshipType, citation, microcitation);
478
        Taxon relatedTaxon = (Taxon)result.getCdmEntity();
479
//        result.setCdmEntity(relatedTaxon);
480
        result.addUpdatedObject(relatedTaxon);
481
        result.addUpdatedObject(toTaxon);
482
        return result;
483
    }
484

    
485
    @Override
486
    @Transactional(readOnly = false)
487
    public UpdateResult changeSynonymToRelatedTaxon(Synonym synonym, Taxon toTaxon, TaxonRelationshipType taxonRelationshipType, Reference citation, String microcitation){
488
        // Get name from synonym
489
        if (synonym == null){
490
            return null;
491
        }
492

    
493
        UpdateResult result = new UpdateResult();
494

    
495
        TaxonName synonymName = synonym.getName();
496

    
497
      /*  // remove synonym from taxon
498
        toTaxon.removeSynonym(synonym);
499
*/
500
        // Create a taxon with synonym name
501
        Taxon fromTaxon = Taxon.NewInstance(synonymName, null);
502
        fromTaxon.setPublish(synonym.isPublish());
503
        save(fromTaxon);
504
        fromTaxon.setAppendedPhrase(synonym.getAppendedPhrase());
505

    
506
        // Add taxon relation
507
        fromTaxon.addTaxonRelation(toTaxon, taxonRelationshipType, citation, microcitation);
508
        result.setCdmEntity(fromTaxon);
509
        // since we are swapping names, we have to detach the name from the synonym completely.
510
        // Otherwise the synonym will still be in the list of typified names.
511
       // synonym.getName().removeTaxonBase(synonym);
512
        result.includeResult(this.deleteSynonym(synonym, null));
513

    
514
        return result;
515
    }
516

    
517
    @Transactional(readOnly = false)
518
    @Override
519
    public void changeHomotypicalGroupOfSynonym(Synonym synonym, HomotypicalGroup newHomotypicalGroup,
520
            Taxon targetTaxon, boolean setBasionymRelationIfApplicable){
521
        // Get synonym name
522
        TaxonName synonymName = synonym.getName();
523
        HomotypicalGroup oldHomotypicalGroup = synonymName.getHomotypicalGroup();
524

    
525
        // Switch groups
526
        oldHomotypicalGroup.removeTypifiedName(synonymName, false);
527
        newHomotypicalGroup.addTypifiedName(synonymName);
528

    
529
        //remove existing basionym relationships
530
        synonymName.removeBasionyms();
531

    
532
        //add basionym relationship
533
        if (setBasionymRelationIfApplicable){
534
            Set<TaxonName> basionyms = newHomotypicalGroup.getBasionyms();
535
            for (TaxonName basionym : basionyms){
536
                synonymName.addBasionym(basionym);
537
            }
538
        }
539

    
540
        //set synonym relationship correctly
541
        Taxon acceptedTaxon = synonym.getAcceptedTaxon();
542

    
543
        boolean hasNewTargetTaxon = targetTaxon != null && !targetTaxon.equals(acceptedTaxon);
544
        if (acceptedTaxon != null){
545

    
546
            HomotypicalGroup acceptedGroup = acceptedTaxon.getHomotypicGroup();
547
            boolean isHomotypicToTaxon = acceptedGroup.equals(newHomotypicalGroup);
548
            SynonymType newRelationType = isHomotypicToTaxon? SynonymType.HOMOTYPIC_SYNONYM_OF() : SynonymType.HETEROTYPIC_SYNONYM_OF();
549
            synonym.setType(newRelationType);
550

    
551
            if (hasNewTargetTaxon){
552
                acceptedTaxon.removeSynonym(synonym, false);
553
            }
554
        }
555
        if (hasNewTargetTaxon ){
556
            @SuppressWarnings("null")
557
            HomotypicalGroup acceptedGroup = targetTaxon.getHomotypicGroup();
558
            boolean isHomotypicToTaxon = acceptedGroup.equals(newHomotypicalGroup);
559
            SynonymType relType = isHomotypicToTaxon? SynonymType.HOMOTYPIC_SYNONYM_OF() : SynonymType.HETEROTYPIC_SYNONYM_OF();
560
            targetTaxon.addSynonym(synonym, relType);
561
        }
562
    }
563

    
564
    @Override
565
    @Transactional(readOnly = false)
566
    public UpdateResult updateCaches(Class<? extends TaxonBase> clazz, Integer stepSize, IIdentifiableEntityCacheStrategy<TaxonBase> cacheStrategy, IProgressMonitor monitor) {
567
        if (clazz == null){
568
            clazz = TaxonBase.class;
569
        }
570
        return super.updateCachesImpl(clazz, stepSize, cacheStrategy, monitor);
571
    }
572

    
573
    @Override
574
    @Autowired
575
    protected void setDao(ITaxonDao dao) {
576
        this.dao = dao;
577
    }
578

    
579
    @Override
580
    public <T extends TaxonBase> Pager<T> findTaxaByName(Class<T> clazz, String uninomial,	String infragenericEpithet, String specificEpithet,
581
            String infraspecificEpithet, String authorshipCache, Rank rank, Integer pageSize,Integer pageNumber, List<String> propertyPaths) {
582
        long numberOfResults = dao.countTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, authorshipCache, rank);
583

    
584
        List<T> results = new ArrayList<>();
585
        if(numberOfResults > 0) { // no point checking again
586
            results = dao.findTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, authorshipCache, rank,
587
                    pageSize, pageNumber, propertyPaths);
588
        }
589

    
590
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
591
    }
592

    
593
    @Override
594
    public <T extends TaxonBase> List<T> listTaxaByName(Class<T> clazz, String uninomial, String infragenericEpithet, String specificEpithet,
595
            String infraspecificEpithet, String authorshipCache, Rank rank, Integer pageSize,Integer pageNumber, List<String> propertyPaths) {
596

    
597
        return findTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infragenericEpithet, authorshipCache, rank,
598
                pageSize, pageNumber, propertyPaths).getRecords();
599
    }
600

    
601
    @Override
602
    public List<TaxonRelationship> listToTaxonRelationships(Taxon taxon, TaxonRelationshipType type,
603
            boolean includeUnpublished, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths){
604
        long numberOfResults = dao.countTaxonRelationships(taxon, type, includeUnpublished, TaxonRelationship.Direction.relatedTo);
605

    
606
        List<TaxonRelationship> results = new ArrayList<>();
607
        if(numberOfResults > 0) { // no point checking again
608
            results = dao.getTaxonRelationships(taxon, type, includeUnpublished, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedTo);
609
        }
610
        return results;
611
    }
612

    
613
    @Override
614
    public Pager<TaxonRelationship> pageToTaxonRelationships(Taxon taxon, TaxonRelationshipType type,
615
            boolean includeUnpublished, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
616
        long numberOfResults = dao.countTaxonRelationships(taxon, type, includeUnpublished, TaxonRelationship.Direction.relatedTo);
617

    
618
        List<TaxonRelationship> results = new ArrayList<>();
619
        if(numberOfResults > 0) { // no point checking again
620
            results = dao.getTaxonRelationships(taxon, type, includeUnpublished, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedTo);
621
        }
622
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
623
    }
624

    
625
    @Override
626
    public List<TaxonRelationship> listFromTaxonRelationships(Taxon taxon, TaxonRelationshipType type,
627
            boolean includeUnpublished, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths){
628
        long numberOfResults = dao.countTaxonRelationships(taxon, type, includeUnpublished, TaxonRelationship.Direction.relatedFrom);
629

    
630
        List<TaxonRelationship> results = new ArrayList<>();
631
        if(numberOfResults > 0) { // no point checking again
632
            results = dao.getTaxonRelationships(taxon, type, includeUnpublished, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedFrom);
633
        }
634
        return results;
635
    }
636

    
637
    @Override
638
    public Pager<TaxonRelationship> pageFromTaxonRelationships(Taxon taxon, TaxonRelationshipType type,
639
            boolean includeUnpublished, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
640
        long numberOfResults = dao.countTaxonRelationships(taxon, type, includeUnpublished, TaxonRelationship.Direction.relatedFrom);
641

    
642
        List<TaxonRelationship> results = new ArrayList<>();
643
        if(numberOfResults > 0) { // no point checking again
644
            results = dao.getTaxonRelationships(taxon, type, includeUnpublished, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedFrom);
645
        }
646
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
647
    }
648

    
649
    @Override
650
    public List<TaxonRelationship> listTaxonRelationships(Set<TaxonRelationshipType> types,
651
            Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
652

    
653
        Long numberOfResults = dao.countTaxonRelationships(types);
654
        List<TaxonRelationship> results = new ArrayList<>();
655
        if(numberOfResults > 0) {
656
            results = dao.getTaxonRelationships(types, pageSize, pageNumber, orderHints, propertyPaths);
657
        }
658
        return results;
659
    }
660

    
661
    @Override
662
    public <S extends TaxonBase> Pager<S> page(Class<S> clazz, List<Restriction<?>> restrictions, Integer pageSize,
663
            Integer pageIndex, List<OrderHint> orderHints, List<String> propertyPaths) {
664
        return page(clazz, restrictions, pageSize, pageIndex, orderHints, propertyPaths, INCLUDE_UNPUBLISHED);
665
    }
666

    
667
    @Override
668
    public <S extends TaxonBase> Pager<S> page(Class<S> clazz, List<Restriction<?>> restrictions, Integer pageSize,
669
            Integer pageIndex, List<OrderHint> orderHints, List<String> propertyPaths, boolean includeUnpublished) {
670

    
671
        List<S> records;
672
        long resultSize = dao.count(clazz, restrictions);
673
        if(AbstractPagerImpl.hasResultsInRange(resultSize, pageIndex, pageSize)){
674
            records = dao.list(clazz, restrictions, pageSize, pageIndex, orderHints, propertyPaths, includeUnpublished);
675
        } else {
676
            records = new ArrayList<>();
677
        }
678
        Pager<S> pager = new DefaultPagerImpl<>(pageIndex, resultSize, pageSize, records);
679
        return pager;
680
    }
681

    
682
    @Override
683
    public Taxon findAcceptedTaxonFor(UUID synonymUuid, UUID classificationUuid,
684
            boolean includeUnpublished, List<String> propertyPaths) throws UnpublishedException{
685

    
686
        Synonym synonym = null;
687

    
688
        try {
689
            synonym = (Synonym) dao.load(synonymUuid);
690
            checkPublished(synonym, includeUnpublished, "Synoym is unpublished.");
691
        } catch (ClassCastException e){
692
            throw new EntityNotFoundException("The TaxonBase entity referenced by " + synonymUuid + " is not a Synonmy");
693
        } catch (NullPointerException e){
694
            throw new EntityNotFoundException("No TaxonBase entity found for " + synonymUuid);
695
        }
696

    
697
        Classification classificationFilter = null;
698
        if(classificationUuid != null){
699
            try {
700
                classificationFilter = classificationDao.load(classificationUuid);
701
            } catch (NullPointerException e){
702
                //TODO not sure, why an NPE should be thrown in the above load method
703
                throw new EntityNotFoundException("No Classification entity found for " + classificationUuid);
704
            }
705
            if(classificationFilter == null){
706
                throw new EntityNotFoundException("No Classification entity found for " + classificationUuid);
707
            }
708
        }
709

    
710
        long count = dao.countAcceptedTaxonFor(synonym, classificationFilter) ;
711
        if(count > 0){
712
            Taxon result = dao.acceptedTaxonFor(synonym, classificationFilter, propertyPaths);
713
            checkPublished(result, includeUnpublished, "Accepted taxon unpublished");
714
            return result;
715
        }else{
716
            return null;
717
        }
718
    }
719

    
720
    @Override
721
    public Set<Taxon> listRelatedTaxa(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, Integer maxDepth,
722
            boolean includeUnpublished, Integer limit, Integer start, List<String> propertyPaths) {
723

    
724
        Set<Taxon> relatedTaxa = collectRelatedTaxa(taxon, includeRelationships, new HashSet<>(), includeUnpublished, maxDepth);
725
        relatedTaxa.remove(taxon);
726
        beanInitializer.initializeAll(relatedTaxa, propertyPaths);
727
        return relatedTaxa;
728
    }
729

    
730
    /**
731
     * Recursively collect related taxa for the given <code>taxon</code> . The returned list will also include the
732
     *  <code>taxon</code> supplied as parameter.
733
     *
734
     * @param taxon
735
     * @param includeRelationships
736
     * @param taxa
737
     * @param maxDepth can be <code>null</code> for infinite depth
738
     * @return
739
     */
740
    private Set<Taxon> collectRelatedTaxa(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, Set<Taxon> taxa,
741
            boolean includeUnpublished, Integer maxDepth) {
742

    
743
        if(taxa.isEmpty()) {
744
            taxa.add(taxon);
745
        }
746

    
747
        if(includeRelationships.isEmpty()){
748
            return taxa;
749
        }
750

    
751
        if(maxDepth != null) {
752
            maxDepth--;
753
        }
754
        if(logger.isDebugEnabled()){
755
            logger.debug("collecting related taxa for " + taxon + " with maxDepth=" + maxDepth);
756
        }
757
        List<TaxonRelationship> taxonRelationships = dao.getTaxonRelationships(taxon,
758
                (Set<TaxonRelationshipType>)null, includeUnpublished, null, null, null, null, null);
759
        for (TaxonRelationship taxRel : taxonRelationships) {
760

    
761
            // skip invalid data
762
            if (taxRel.getToTaxon() == null || taxRel.getFromTaxon() == null || taxRel.getType() == null) {
763
                continue;
764
            }
765
            // filter by includeRelationships
766
            for (TaxonRelationshipEdge relationshipEdgeFilter : includeRelationships) {
767
                if ( relationshipEdgeFilter.getRelationshipTypes().equals(taxRel.getType()) ) {
768
                    if (relationshipEdgeFilter.getDirections().contains(Direction.relatedTo) && !taxa.contains(taxRel.getToTaxon())) {
769
                        if(logger.isDebugEnabled()){
770
                            logger.debug(maxDepth + ": " + taxon.getTitleCache() + " --[" + taxRel.getType().getLabel() + "]--> " + taxRel.getToTaxon().getTitleCache());
771
                        }
772
                        taxa.add(taxRel.getToTaxon());
773
                        if(maxDepth == null || maxDepth > 0) {
774
                            taxa.addAll(collectRelatedTaxa(taxRel.getToTaxon(), includeRelationships, taxa, includeUnpublished, maxDepth));
775
                        }
776
                    }
777
                    if(relationshipEdgeFilter.getDirections().contains(Direction.relatedFrom) && !taxa.contains(taxRel.getFromTaxon())) {
778
                        taxa.add(taxRel.getFromTaxon());
779
                        if(logger.isDebugEnabled()){
780
                            logger.debug(maxDepth + ": " +taxRel.getFromTaxon().getTitleCache() + " --[" + taxRel.getType().getLabel() + "]--> " + taxon.getTitleCache() );
781
                        }
782
                        if(maxDepth == null || maxDepth > 0) {
783
                            taxa.addAll(collectRelatedTaxa(taxRel.getFromTaxon(), includeRelationships, taxa, includeUnpublished, maxDepth));
784
                        }
785
                    }
786
                }
787
            }
788
        }
789
        return taxa;
790
    }
791

    
792
    @Override
793
    public Pager<Synonym> getSynonyms(Taxon taxon,	SynonymType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
794
        Long numberOfResults = dao.countSynonyms(taxon, type);
795

    
796
        List<Synonym> results = new ArrayList<>();
797
        if(numberOfResults > 0) { // no point checking again
798
            results = dao.getSynonyms(taxon, type, pageSize, pageNumber, orderHints, propertyPaths);
799
        }
800

    
801
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
802
    }
803

    
804
    @Override
805
    public List<List<Synonym>> getSynonymsByHomotypicGroup(Taxon taxon, List<String> propertyPaths){
806
        List<List<Synonym>> result = new ArrayList<>();
807
        taxon = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
808
        HomotypicGroupTaxonComparator comparator = new HomotypicGroupTaxonComparator(taxon);
809

    
810
        //homotypic
811
        result.add(taxon.getHomotypicSynonymsByHomotypicGroup(comparator));
812

    
813
        //heterotypic
814
        List<HomotypicalGroup> homotypicalGroups = taxon.getHeterotypicSynonymyGroups();  //currently the list is sorted by the Taxon.defaultTaxonComparator
815
        for(HomotypicalGroup homotypicalGroup : homotypicalGroups){
816
            result.add(taxon.getSynonymsInGroup(homotypicalGroup, comparator));
817
        }
818

    
819
        return result;
820
    }
821

    
822
    @Override
823
    public List<Synonym> getHomotypicSynonymsByHomotypicGroup(Taxon taxon, List<String> propertyPaths){
824
        Taxon t = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
825
        HomotypicGroupTaxonComparator comparator = new HomotypicGroupTaxonComparator(taxon);
826

    
827
        return t.getHomotypicSynonymsByHomotypicGroup(comparator);
828
    }
829

    
830
    @Override
831
    public List<List<Synonym>> getHeterotypicSynonymyGroups(Taxon taxon, List<String> propertyPaths){
832
        Taxon t = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
833
        List<HomotypicalGroup> homotypicalGroups = t.getHeterotypicSynonymyGroups();
834
        List<List<Synonym>> heterotypicSynonymyGroups = new ArrayList<>(homotypicalGroups.size());
835
        for(HomotypicalGroup homotypicalGroup : homotypicalGroups){
836
            heterotypicSynonymyGroups.add(t.getSynonymsInGroup(homotypicalGroup));
837
        }
838
        return heterotypicSynonymyGroups;
839
    }
840

    
841
    @Override
842
    public List<UuidAndTitleCache<? extends IdentifiableEntity>> findTaxaAndNamesForEditor(IFindTaxaAndNamesConfigurator config){
843

    
844
        if (config.isDoSynonyms() || config.isDoTaxa() || config.isDoNamesWithoutTaxa() || config.isDoTaxaByCommonNames()){
845
        	return dao.getTaxaByNameForEditor(config.isDoTaxa(), config.isDoSynonyms(), config.isDoNamesWithoutTaxa(),
846
        	        config.isDoMisappliedNames(), config.isDoTaxaByCommonNames(), config.isIncludeUnpublished(), config.isDoIncludeAuthors(),
847
        	        config.getTitleSearchStringSqlized(), config.getClassification(), config.getSubtree(),
848
        	        config.getMatchMode(), config.getNamedAreas(), config.getOrder());
849
        }else{
850
            return new ArrayList<>();
851
        }
852
    }
853

    
854
    @Override
855
    public Pager<IdentifiableEntity> findTaxaAndNames(IFindTaxaAndNamesConfigurator configurator) {
856

    
857
        @SuppressWarnings("rawtypes")
858
        List<IdentifiableEntity> results = new ArrayList<>();
859
        long numberOfResults = 0; // overall number of results (as opposed to number of results per page)
860
        List<TaxonBase> taxa = null;
861

    
862
        // Taxa and synonyms
863
        long numberTaxaResults = 0L;
864

    
865
        List<String> propertyPath = new ArrayList<>();
866
        if(configurator.getTaxonPropertyPath() != null){
867
            propertyPath.addAll(configurator.getTaxonPropertyPath());
868
        }
869

    
870
        if (configurator.isDoMisappliedNames() || configurator.isDoSynonyms() || configurator.isDoTaxa() || configurator.isDoTaxaByCommonNames()){
871
            if(configurator.getPageSize() != null){ // no point counting if we need all anyway
872
                numberTaxaResults =
873
                    dao.countTaxaByName(configurator.isDoTaxa(),configurator.isDoSynonyms(), configurator.isDoMisappliedNames(),
874
                        configurator.isDoTaxaByCommonNames(), configurator.isDoIncludeAuthors(), configurator.getTitleSearchStringSqlized(),
875
                        configurator.getClassification(), configurator.getSubtree(), configurator.getMatchMode(),
876
                        configurator.getNamedAreas(), configurator.isIncludeUnpublished());
877
            }
878

    
879
            if(configurator.getPageSize() == null || numberTaxaResults > configurator.getPageSize() * configurator.getPageNumber()){ // no point checking again if less results
880
                taxa = dao.getTaxaByName(configurator.isDoTaxa(), configurator.isDoSynonyms(),
881
                    configurator.isDoMisappliedNames(), configurator.isDoTaxaByCommonNames(), configurator.isDoIncludeAuthors(),
882
                    configurator.getTitleSearchStringSqlized(), configurator.getClassification(), configurator.getSubtree(),
883
                    configurator.getMatchMode(), configurator.getNamedAreas(), configurator.isIncludeUnpublished(),
884
                    configurator.getOrder(), configurator.getPageSize(), configurator.getPageNumber(), propertyPath);
885
            }
886
        }
887

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

    
890
        if(taxa != null){
891
            results.addAll(taxa);
892
        }
893

    
894
        numberOfResults += numberTaxaResults;
895

    
896
        // Names without taxa
897
        if (configurator.isDoNamesWithoutTaxa()) {
898
            int numberNameResults = 0;
899

    
900
            List<TaxonName> names =
901
                nameDao.findByName(configurator.isDoIncludeAuthors(), configurator.getTitleSearchStringSqlized(), configurator.getMatchMode(),
902
                        configurator.getPageSize(), configurator.getPageNumber(), null, configurator.getTaxonNamePropertyPath());
903
            if (logger.isDebugEnabled()) { logger.debug(names.size() + " matching name(s) found"); }
904
            if (names.size() > 0) {
905
                for (TaxonName taxonName : names) {
906
                    if (taxonName.getTaxonBases().size() == 0) {
907
                        results.add(taxonName);
908
                        numberNameResults++;
909
                    }
910
                }
911
                if (logger.isDebugEnabled()) { logger.debug(numberNameResults + " matching name(s) without taxa found"); }
912
                numberOfResults += numberNameResults;
913
            }
914
        }
915

    
916
       return new DefaultPagerImpl<> (configurator.getPageNumber(), numberOfResults, configurator.getPageSize(), results);
917
    }
918

    
919
    public List<UuidAndTitleCache<TaxonBase>> getTaxonUuidAndTitleCache(Integer limit, String pattern){
920
        return dao.getUuidAndTitleCache(limit, pattern);
921
    }
922

    
923
    @Override
924
    public List<Media> listTaxonDescriptionMedia(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, boolean limitToGalleries, List<String> propertyPath){
925
        return listMedia(taxon, includeRelationships, limitToGalleries, true, false, false, propertyPath);
926
    }
927

    
928
    @Override
929
    public List<Media> listMedia(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships,
930
            Boolean limitToGalleries, Boolean includeTaxonDescriptions, Boolean includeOccurrences,
931
            Boolean includeTaxonNameDescriptions, List<String> propertyPath) {
932

    
933
        //TODO let inherit
934
        boolean includeUnpublished = INCLUDE_UNPUBLISHED;
935

    
936
    //    logger.setLevel(Level.TRACE);
937
//        Logger.getLogger("org.hibernate.SQL").setLevel(Level.TRACE);
938

    
939
        logger.trace("listMedia() - START");
940

    
941
        Set<Taxon> taxa = new HashSet<>();
942
        List<Media> taxonMedia = new ArrayList<>();
943
        List<Media> nonImageGalleryImages = new ArrayList<>();
944

    
945
        if (limitToGalleries == null) {
946
            limitToGalleries = false;
947
        }
948

    
949
        // --- resolve related taxa
950
        if (includeRelationships != null && ! includeRelationships.isEmpty()) {
951
            logger.trace("listMedia() - resolve related taxa");
952
            taxa = listRelatedTaxa(taxon, includeRelationships, null, includeUnpublished, null, null, null);
953
        }
954

    
955
        taxa.add((Taxon) dao.load(taxon.getUuid()));
956

    
957
        if(includeTaxonDescriptions != null && includeTaxonDescriptions){
958
            logger.trace("listMedia() - includeTaxonDescriptions");
959
            List<TaxonDescription> taxonDescriptions = new ArrayList<>();
960
            // --- TaxonDescriptions
961
            for (Taxon t : taxa) {
962
                taxonDescriptions.addAll(descriptionService.listTaxonDescriptions(t, null, null, null, null, propertyPath));
963
            }
964
            for (TaxonDescription taxonDescription : taxonDescriptions) {
965
                if (!limitToGalleries || taxonDescription.isImageGallery()) {
966
                    for (DescriptionElementBase element : taxonDescription.getElements()) {
967
                        for (Media media : element.getMedia()) {
968
                            if(taxonDescription.isImageGallery()){
969
                                taxonMedia.add(media);
970
                            }
971
                            else{
972
                                nonImageGalleryImages.add(media);
973
                            }
974
                        }
975
                    }
976
                }
977
            }
978
            //put images from image gallery first (#3242)
979
            taxonMedia.addAll(nonImageGalleryImages);
980
        }
981

    
982

    
983
        if(includeOccurrences != null && includeOccurrences) {
984
            logger.trace("listMedia() - includeOccurrences");
985
            @SuppressWarnings("rawtypes")
986
            Set<SpecimenOrObservationBase> specimensOrObservations = new HashSet<>();
987
            // --- Specimens
988
            for (Taxon t : taxa) {
989
                specimensOrObservations.addAll(occurrenceDao.listByAssociatedTaxon(null, t, null, null, null, null));
990
            }
991
            for (SpecimenOrObservationBase<?> occurrence : specimensOrObservations) {
992

    
993
//            	direct media removed from specimen #3597
994
//              taxonMedia.addAll(occurrence.getMedia());
995

    
996
                // SpecimenDescriptions
997
                Set<SpecimenDescription> specimenDescriptions = occurrence.getSpecimenDescriptions();
998
                for (DescriptionBase<?> specimenDescription : specimenDescriptions) {
999
                    if (!limitToGalleries || specimenDescription.isImageGallery()) {
1000
                        Set<DescriptionElementBase> elements = specimenDescription.getElements();
1001
                        for (DescriptionElementBase element : elements) {
1002
                            for (Media media : element.getMedia()) {
1003
                                taxonMedia.add(media);
1004
                            }
1005
                        }
1006
                    }
1007
                }
1008

    
1009
                if (occurrence.isInstanceOf(DerivedUnit.class)) {
1010
                    DerivedUnit derivedUnit = CdmBase.deproxy(occurrence, DerivedUnit.class);
1011
                    // Collection
1012
                    //TODO why may collections have media attached? #
1013
                    if (derivedUnit.getCollection() != null){
1014
                        taxonMedia.addAll(derivedUnit.getCollection().getMedia());
1015
                    }
1016
                }
1017
                //media in hierarchy
1018
                taxonMedia.addAll(occurrenceService.getMediainHierarchy(occurrence, null, null, propertyPath).getRecords());
1019
            }
1020
        }
1021

    
1022
        if(includeTaxonNameDescriptions != null && includeTaxonNameDescriptions) {
1023
            logger.trace("listMedia() - includeTaxonNameDescriptions");
1024
            // --- TaxonNameDescription
1025
            Set<TaxonNameDescription> nameDescriptions = new HashSet<>();
1026
            for (Taxon t : taxa) {
1027
                nameDescriptions .addAll(t.getName().getDescriptions());
1028
            }
1029
            for(TaxonNameDescription nameDescription: nameDescriptions){
1030
                if (!limitToGalleries || nameDescription.isImageGallery()) {
1031
                    Set<DescriptionElementBase> elements = nameDescription.getElements();
1032
                    for (DescriptionElementBase element : elements) {
1033
                        for (Media media : element.getMedia()) {
1034
                            taxonMedia.add(media);
1035
                        }
1036
                    }
1037
                }
1038
            }
1039
        }
1040

    
1041
        logger.trace("listMedia() - initialize");
1042
        beanInitializer.initializeAll(taxonMedia, propertyPath);
1043

    
1044
        logger.trace("listMedia() - END");
1045

    
1046
        return taxonMedia;
1047
    }
1048

    
1049
    @Override
1050
    public List<TaxonBase> findTaxaByID(Set<Integer> listOfIDs) {
1051
        return this.dao.loadList(listOfIDs, null, null);
1052
    }
1053

    
1054
    @Override
1055
    public TaxonBase findTaxonByUuid(UUID uuid, List<String> propertyPaths){
1056
        return this.dao.findByUuid(uuid, null ,propertyPaths);
1057
    }
1058

    
1059
    @Override
1060
    public long countSynonyms(boolean onlyAttachedToTaxon){
1061
        return this.dao.countSynonyms(onlyAttachedToTaxon);
1062
    }
1063

    
1064
    @Override
1065
    @Transactional(readOnly=false)
1066
    public DeleteResult deleteTaxon(UUID taxonUUID, TaxonDeletionConfigurator config, UUID classificationUuid)  {
1067

    
1068
    	if (config == null){
1069
            config = new TaxonDeletionConfigurator();
1070
        }
1071
    	Taxon taxon = (Taxon)dao.load(taxonUUID);
1072
    	DeleteResult result = new DeleteResult();
1073
    	if (taxon == null){
1074
    	    result.setAbort();
1075
    	    result.addException(new Exception ("The taxon was already deleted."));
1076
    	    return result;
1077
    	}
1078
    	taxon = HibernateProxyHelper.deproxy(taxon);
1079
    	Classification classification = HibernateProxyHelper.deproxy(classificationDao.load(classificationUuid), Classification.class);
1080
    	config.setClassificationUuid(classificationUuid);
1081
        result = isDeletable(taxonUUID, config);
1082

    
1083
        if (result.isOk()){
1084
            // --- DeleteSynonymRelations
1085
            if (config.isDeleteSynonymRelations()){
1086
                boolean removeSynonymNameFromHomotypicalGroup = false;
1087
                // use tmp Set to avoid concurrent modification
1088
                Set<Synonym> synsToDelete = new HashSet<>();
1089
                synsToDelete.addAll(taxon.getSynonyms());
1090
                for (Synonym synonym : synsToDelete){
1091
                    taxon.removeSynonym(synonym, removeSynonymNameFromHomotypicalGroup);
1092

    
1093
                    // --- DeleteSynonymsIfPossible
1094
                    if (config.isDeleteSynonymsIfPossible()){
1095
                        //TODO which value
1096
                        boolean newHomotypicGroupIfNeeded = true;
1097
                        SynonymDeletionConfigurator synConfig = new SynonymDeletionConfigurator();
1098
                        result.includeResult(deleteSynonym(synonym, synConfig));
1099
                    }
1100
                }
1101
            }
1102

    
1103
            // --- DeleteTaxonRelationships
1104
            if (! config.isDeleteTaxonRelationships()){
1105
                if (taxon.getTaxonRelations().size() > 0){
1106
                    result.setAbort();
1107
                    result.addException(new Exception("Taxon can't be deleted as it is related to another taxon. " +
1108
                            "Remove taxon from all relations to other taxa prior to deletion."));
1109
                }
1110
            } else{
1111
                TaxonDeletionConfigurator configRelTaxon = new TaxonDeletionConfigurator();
1112
                configRelTaxon.setDeleteTaxonNodes(false);
1113
                configRelTaxon.setDeleteConceptRelationships(true);
1114

    
1115
                for (TaxonRelationship taxRel: taxon.getTaxonRelations()){
1116
                    if (config.isDeleteMisappliedNames()
1117
                            && taxRel.getType().isMisappliedName()
1118
                            && taxon.equals(taxRel.getToTaxon())){
1119
                        this.deleteTaxon(taxRel.getFromTaxon().getUuid(), config, classificationUuid);
1120
                    } else if (config.isDeleteConceptRelationships() && taxRel.getType().isConceptRelationship()){
1121

    
1122
                        if (taxon.equals(taxRel.getToTaxon()) && isDeletable(taxRel.getFromTaxon().getUuid(), configRelTaxon).isOk()){
1123
                            this.deleteTaxon(taxRel.getFromTaxon().getUuid(), configRelTaxon, classificationUuid);
1124
                        }else if (isDeletable(taxRel.getToTaxon().getUuid(), configRelTaxon).isOk()){
1125
                            this.deleteTaxon(taxRel.getToTaxon().getUuid(), configRelTaxon, classificationUuid);
1126
                        }
1127
                    }
1128
                    taxon.removeTaxonRelation(taxRel);
1129
                }
1130
            }
1131

    
1132
            //    	TaxonDescription
1133
            if (config.isDeleteDescriptions()){
1134
                Set<TaxonDescription> descriptions = taxon.getDescriptions();
1135
                List<TaxonDescription> removeDescriptions = new ArrayList<>();
1136
                for (TaxonDescription desc: descriptions){
1137
                    //TODO use description delete configurator ?
1138
                    //FIXME check if description is ALWAYS deletable
1139
                    if (desc.getDescribedSpecimenOrObservation() != null){
1140
                        result.setAbort();
1141
                        result.addException(new Exception("Taxon can't be deleted as it is used in a TaxonDescription" +
1142
                                " which also describes specimens or observations"));
1143
                        break;
1144
                    }
1145
                    removeDescriptions.add(desc);
1146
                }
1147
                if (result.isOk()){
1148
                    for (TaxonDescription desc: removeDescriptions){
1149
                        taxon.removeDescription(desc);
1150
                        descriptionService.delete(desc);
1151
                    }
1152
                } else {
1153
                    return result;
1154
                }
1155
            }
1156

    
1157
             if (! config.isDeleteTaxonNodes() || (!config.isDeleteInAllClassifications() && classification == null && taxon.getTaxonNodes().size() > 1)){
1158
                 result.addException(new Exception( "Taxon can't be deleted as it is used in more than one classification."));
1159
             }else{
1160
                 if (taxon.getTaxonNodes().size() != 0){
1161
                    Set<TaxonNode> nodes = taxon.getTaxonNodes();
1162
                    Iterator<TaxonNode> iterator = nodes.iterator();
1163
                    TaxonNode node = null;
1164
                    boolean deleteChildren;
1165
                    if (config.getTaxonNodeConfig().getChildHandling().equals(ChildHandling.DELETE)){
1166
                        deleteChildren = true;
1167
                    }else {
1168
                        deleteChildren = false;
1169
                    }
1170
                    boolean success = true;
1171
                    if (!config.isDeleteInAllClassifications() && !(classification == null)){
1172
                        while (iterator.hasNext()){
1173
                            node = iterator.next();
1174
                            if (node.getClassification().equals(classification)){
1175
                                break;
1176
                            }
1177
                            node = null;
1178
                        }
1179
                        if (node != null){
1180
                            HibernateProxyHelper.deproxy(node, TaxonNode.class);
1181
                            success =taxon.removeTaxonNode(node, deleteChildren);
1182
                            nodeService.delete(node);
1183
                            result.addDeletedObject(node);
1184
                        } else {
1185
                        	result.setError();
1186
                        	result.addException(new Exception("The taxon can not be deleted because it is not used in defined classification."));
1187
                        }
1188
                    } else if (config.isDeleteInAllClassifications()){
1189
                        List<TaxonNode> nodesList = new ArrayList<>();
1190
                        nodesList.addAll(taxon.getTaxonNodes());
1191
                        for (ITaxonTreeNode treeNode: nodesList){
1192
                            TaxonNode taxonNode = (TaxonNode) treeNode;
1193
                            if(!deleteChildren){
1194
                                Object[] childNodes = taxonNode.getChildNodes().toArray();
1195
                                for (Object childNode: childNodes){
1196
                                    TaxonNode childNodeCast = (TaxonNode) childNode;
1197
                                    taxonNode.getParent().addChildNode(childNodeCast, childNodeCast.getReference(), childNodeCast.getMicroReference());
1198
                                }
1199
                            }
1200
                        }
1201
                        config.getTaxonNodeConfig().setDeleteElement(false);
1202
                        DeleteResult resultNodes = nodeService.deleteTaxonNodes(nodesList, config);
1203
                        if (!resultNodes.isOk()){
1204
                        	result.addExceptions(resultNodes.getExceptions());
1205
                        	result.setStatus(resultNodes.getStatus());
1206
                        } else {
1207
                            result.addUpdatedObjects(resultNodes.getUpdatedObjects());
1208
                        }
1209
                    }
1210
                    if (!success){
1211
                        result.setError();
1212
                        result.addException(new Exception("The taxon can not be deleted because the taxon node can not be removed."));
1213
                    }
1214
                }
1215
             }
1216
             TaxonName name = taxon.getName();
1217
             taxon.setName(null);
1218
             this.saveOrUpdate(taxon);
1219

    
1220
             if ((taxon.getTaxonNodes() == null || taxon.getTaxonNodes().size()== 0)  && result.isOk()){
1221
                 try{
1222
                     dao.delete(taxon);
1223
                     result.addDeletedObject(taxon);
1224
                 }catch(Exception e){
1225
                     result.addException(e);
1226
                     result.setError();
1227
                 }
1228
             } else {
1229
                 result.setError();
1230
                 result.addException(new Exception("The Taxon can't be deleted because it is used in a classification."));
1231

    
1232
             }
1233
             //TaxonName
1234
             if (config.isDeleteNameIfPossible() && result.isOk()){
1235
                 DeleteResult nameResult = new DeleteResult();
1236
                 //remove name if possible (and required)
1237
                 if (name != null ){
1238
                     nameResult = nameService.delete(name.getUuid(), config.getNameDeletionConfig());
1239
                 }
1240
                 if (nameResult.isError() || nameResult.isAbort()){
1241
                     result.addRelatedObject(name);
1242
                     result.addExceptions(nameResult.getExceptions());
1243
                 }else{
1244
                     result.includeResult(nameResult);
1245
                 }
1246
             }
1247
       }
1248

    
1249
       return result;
1250
    }
1251

    
1252
    @Override
1253
    @Transactional(readOnly = false)
1254
    public DeleteResult delete(UUID synUUID){
1255
    	Synonym syn = (Synonym)dao.load(synUUID);
1256
        return this.deleteSynonym(syn, null);
1257
    }
1258

    
1259
    @Override
1260
    @Transactional(readOnly = false)
1261
    public DeleteResult deleteSynonym(UUID synonymUuid, SynonymDeletionConfigurator config) {
1262
        return deleteSynonym((Synonym)dao.load(synonymUuid), config);
1263
    }
1264

    
1265
    @Override
1266
    @Transactional(readOnly = false)
1267
    public DeleteResult deleteSynonym(Synonym synonym, SynonymDeletionConfigurator config) {
1268
        DeleteResult result = new DeleteResult();
1269
    	if (synonym == null){
1270
    		result.setAbort();
1271
    		result.addException(new Exception("The synonym was already deleted."));
1272
    		return result;
1273
        }
1274

    
1275
        if (config == null){
1276
            config = new SynonymDeletionConfigurator();
1277
        }
1278

    
1279
        result = isDeletable(synonym.getUuid(), config);
1280

    
1281
        if (result.isOk()){
1282

    
1283
            synonym = HibernateProxyHelper.deproxy(this.load(synonym.getUuid()), Synonym.class);
1284

    
1285
            //remove synonym
1286
            Taxon accTaxon = synonym.getAcceptedTaxon();
1287

    
1288
            if (accTaxon != null){
1289
                accTaxon = HibernateProxyHelper.deproxy(accTaxon, Taxon.class);
1290
                accTaxon.removeSynonym(synonym, false);
1291
                this.saveOrUpdate(accTaxon);
1292
                result.addUpdatedObject(accTaxon);
1293
            }
1294
            this.saveOrUpdate(synonym);
1295
            //#6281
1296
            dao.flush();
1297

    
1298
            TaxonName name = synonym.getName();
1299
            synonym.setName(null);
1300

    
1301
            dao.delete(synonym);
1302
            result.addDeletedObject(synonym);
1303

    
1304
            //remove name if possible (and required)
1305
            if (name != null && config.isDeleteNameIfPossible()){
1306

    
1307
                DeleteResult nameDeleteResult = nameService.delete(name, config.getNameDeletionConfig());
1308
                if (nameDeleteResult.isAbort() || nameDeleteResult.isError()){
1309
                	result.addExceptions(nameDeleteResult.getExceptions());
1310
                	result.addRelatedObject(name);
1311
                	result.addUpdatedObject(name);
1312
                }else{
1313
                    result.addDeletedObject(name);
1314
                }
1315
            }
1316
        }
1317
        return result;
1318
    }
1319

    
1320
    @Override
1321
    public Map<String, Map<UUID,Set<TaxonName>>> findIdenticalTaxonNames(List<UUID> sourceRefUuids, List<String> propertyPaths) {
1322
        return this.dao.findIdenticalNames(sourceRefUuids, propertyPaths);
1323
    }
1324

    
1325
    @Override
1326
    public Taxon findBestMatchingTaxon(String taxonName) {
1327
        MatchingTaxonConfigurator config = MatchingTaxonConfigurator.NewInstance();
1328
        config.setTaxonNameTitle(taxonName);
1329
        return findBestMatchingTaxon(config);
1330
    }
1331

    
1332
    @Override
1333
    public Taxon findBestMatchingTaxon(MatchingTaxonConfigurator config) {
1334

    
1335
        Taxon bestCandidate = null;
1336
        try{
1337
            // 1. search for accepted taxa
1338
            List<TaxonBase> taxonList = dao.findByNameTitleCache(true, false, config.isIncludeUnpublished(),
1339
                    config.getTaxonNameTitle(), null, null, MatchMode.EXACT, null, null, 0, null, null);
1340
            boolean bestCandidateMatchesSecUuid = false;
1341
            boolean bestCandidateIsInClassification = false;
1342
            int countEqualCandidates = 0;
1343
            for(TaxonBase<?> taxonBaseCandidate : taxonList){
1344
                if(taxonBaseCandidate instanceof Taxon){
1345
                    Taxon newCanditate = CdmBase.deproxy(taxonBaseCandidate, Taxon.class);
1346
                    boolean newCandidateMatchesSecUuid = isMatchesSecUuid(newCanditate, config);
1347
                    if (! newCandidateMatchesSecUuid && config.isOnlyMatchingSecUuid() ){
1348
                        continue;
1349
                    }else if(newCandidateMatchesSecUuid && ! bestCandidateMatchesSecUuid){
1350
                        bestCandidate = newCanditate;
1351
                        countEqualCandidates = 1;
1352
                        bestCandidateMatchesSecUuid = true;
1353
                        continue;
1354
                    }
1355

    
1356
                    boolean newCandidateInClassification = isInClassification(newCanditate, config);
1357
                    if (! newCandidateInClassification && config.isOnlyMatchingClassificationUuid()){
1358
                        continue;
1359
                    }else if (newCandidateInClassification && ! bestCandidateIsInClassification){
1360
                        bestCandidate = newCanditate;
1361
                        countEqualCandidates = 1;
1362
                        bestCandidateIsInClassification = true;
1363
                        continue;
1364
                    }
1365
                    if (bestCandidate == null){
1366
                        bestCandidate = newCanditate;
1367
                        countEqualCandidates = 1;
1368
                        continue;
1369
                    }
1370
                }else{  //not Taxon.class
1371
                    continue;
1372
                }
1373
                countEqualCandidates++;
1374

    
1375
            }
1376
            if (bestCandidate != null){
1377
                if(countEqualCandidates > 1){
1378
                    logger.info(countEqualCandidates + " equally matching TaxonBases found, using first accepted Taxon: " + bestCandidate.getTitleCache());
1379
                    return bestCandidate;
1380
                } else {
1381
                    logger.info("using accepted Taxon: " + bestCandidate.getTitleCache());
1382
                    return bestCandidate;
1383
                }
1384
            }
1385

    
1386
            // 2. search for synonyms
1387
            if (config.isIncludeSynonyms()){
1388
                List<TaxonBase> synonymList = dao.findByNameTitleCache(false, true, config.isIncludeUnpublished(),
1389
                        config.getTaxonNameTitle(), null, null, MatchMode.EXACT, null, null, 0, null, null);
1390
                for(TaxonBase taxonBase : synonymList){
1391
                    if(taxonBase instanceof Synonym){
1392
                        Synonym synonym = CdmBase.deproxy(taxonBase, Synonym.class);
1393
                        bestCandidate = synonym.getAcceptedTaxon();
1394
                        if(bestCandidate != null){
1395
                            logger.info("using accepted Taxon " +  bestCandidate.getTitleCache() + " for synonym " + taxonBase.getTitleCache());
1396
                            return bestCandidate;
1397
                        }
1398
                        //TODO extend method: search using treeUUID, using SecUUID, first find accepted then include synonyms until a matching taxon is found
1399
                    }
1400
                }
1401
            }
1402

    
1403
        } catch (Exception e){
1404
            logger.error(e);
1405
            e.printStackTrace();
1406
        }
1407

    
1408
        return bestCandidate;
1409
    }
1410

    
1411
    private boolean isInClassification(Taxon taxon, MatchingTaxonConfigurator config) {
1412
        UUID configClassificationUuid = config.getClassificationUuid();
1413
        if (configClassificationUuid == null){
1414
            return false;
1415
        }
1416
        for (TaxonNode node : taxon.getTaxonNodes()){
1417
            UUID classUuid = node.getClassification().getUuid();
1418
            if (configClassificationUuid.equals(classUuid)){
1419
                return true;
1420
            }
1421
        }
1422
        return false;
1423
    }
1424

    
1425
    private boolean isMatchesSecUuid(Taxon taxon, MatchingTaxonConfigurator config) {
1426
        UUID configSecUuid = config.getSecUuid();
1427
        if (configSecUuid == null){
1428
            return false;
1429
        }
1430
        UUID taxonSecUuid = (taxon.getSec() == null)? null : taxon.getSec().getUuid();
1431
        return configSecUuid.equals(taxonSecUuid);
1432
    }
1433

    
1434
    @Override
1435
    public Synonym findBestMatchingSynonym(String taxonName, boolean includeUnpublished) {
1436
        List<TaxonBase> synonymList = dao.findByNameTitleCache(false, true, includeUnpublished, taxonName, null, null, MatchMode.EXACT, null, null, 0, null, null);
1437
        if(! synonymList.isEmpty()){
1438
            Synonym result = CdmBase.deproxy(synonymList.iterator().next(), Synonym.class);
1439
            if(synonymList.size() == 1){
1440
                logger.info(synonymList.size() + " Synonym found " + result.getTitleCache() );
1441
                return result;
1442
            } else {
1443
                logger.info("Several matching synonyms found. Using first: " +  result.getTitleCache());
1444
                return result;
1445
            }
1446
        }
1447
        return null;
1448
    }
1449

    
1450
    @Override
1451
    @Transactional(readOnly = false)
1452
    public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym, UUID newTaxonUUID, boolean moveHomotypicGroup,
1453
            SynonymType newSynonymType, UUID newSecundumUuid, String newSecundumDetail,
1454
            boolean keepSecundumIfUndefined) throws HomotypicalGroupChangeException {
1455

    
1456
        UpdateResult result = new UpdateResult();
1457
        Taxon newTaxon = CdmBase.deproxy(dao.load(newTaxonUUID), Taxon.class);
1458
        result = moveSynonymToAnotherTaxon(oldSynonym, newTaxon, moveHomotypicGroup, newSynonymType,
1459
                newSecundumUuid, newSecundumDetail, keepSecundumIfUndefined);
1460

    
1461
        return result;
1462
    }
1463

    
1464
    @Override
1465
    @Transactional(readOnly = false)
1466
    public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym,
1467
            Taxon newTaxon,
1468
            boolean moveHomotypicGroup,
1469
            SynonymType newSynonymType) throws HomotypicalGroupChangeException {
1470
        return moveSynonymToAnotherTaxon(oldSynonym, newTaxon, moveHomotypicGroup,
1471
                newSynonymType,
1472
                oldSynonym.getSec()!= null? oldSynonym.getSec().getUuid(): null,
1473
                oldSynonym.getSecMicroReference(),
1474
                true);
1475
    }
1476

    
1477
    @Override
1478
    @Transactional(readOnly = false)
1479
    public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym,
1480
            Taxon newTaxon,
1481
            boolean moveHomotypicGroup,
1482
            SynonymType newSynonymType,
1483
            UUID newSecundumUuid,
1484
            String newSecundumDetail,
1485
            boolean keepSecundumIfUndefined) throws HomotypicalGroupChangeException {
1486

    
1487
        Synonym synonym = CdmBase.deproxy(dao.load(oldSynonym.getUuid()), Synonym.class);
1488
        Taxon oldTaxon = CdmBase.deproxy(dao.load(synonym.getAcceptedTaxon().getUuid()), Taxon.class);
1489
        //TODO what if there is no name ?? Concepts may be cached (e.g. via TCS import)
1490
        TaxonName synonymName = synonym.getName();
1491
        TaxonName fromTaxonName = oldTaxon.getName();
1492
        //set default relationship type
1493
        if (newSynonymType == null){
1494
            newSynonymType = SynonymType.HETEROTYPIC_SYNONYM_OF();
1495
        }
1496
        boolean newRelTypeIsHomotypic = newSynonymType.equals(SynonymType.HOMOTYPIC_SYNONYM_OF());
1497

    
1498
        HomotypicalGroup homotypicGroup = synonymName.getHomotypicalGroup();
1499
        int hgSize = homotypicGroup.getTypifiedNames().size();
1500
        boolean isSingleInGroup = !(hgSize > 1);
1501

    
1502
        if (! isSingleInGroup){
1503
            boolean isHomotypicToAccepted = synonymName.isHomotypic(fromTaxonName);
1504
            boolean hasHomotypicSynonymRelatives = isHomotypicToAccepted ? hgSize > 2 : hgSize > 1;
1505
            if (isHomotypicToAccepted){
1506
                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.";
1507
                String homotypicRelatives = hasHomotypicSynonymRelatives ? " and other synonym(s)":"";
1508
                message = String.format(message, homotypicRelatives);
1509
                throw new HomotypicalGroupChangeException(message);
1510
            }
1511
            if (! moveHomotypicGroup){
1512
                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.";
1513
                throw new HomotypicalGroupChangeException(message);
1514
            }
1515
        }else{
1516
            moveHomotypicGroup = true;  //single synonym always allows to moveCompleteGroup
1517
        }
1518
//        Assert.assertTrue("Synonym can only be moved with complete homotypic group", moveHomotypicGroup);
1519

    
1520
        UpdateResult result = new UpdateResult();
1521
        //move all synonyms to new taxon
1522
        List<Synonym> homotypicSynonyms = oldTaxon.getSynonymsInGroup(homotypicGroup);
1523
        Reference newSecundum = referenceService.load(newSecundumUuid);
1524
        for (Synonym synRelation: homotypicSynonyms){
1525

    
1526
            newTaxon = HibernateProxyHelper.deproxy(newTaxon, Taxon.class);
1527
            oldTaxon = HibernateProxyHelper.deproxy(oldTaxon, Taxon.class);
1528
            oldTaxon.removeSynonym(synRelation, false);
1529
            newTaxon.addSynonym(synRelation, newSynonymType);
1530

    
1531
            if (newSecundum != null || !keepSecundumIfUndefined){
1532
                synRelation.setSec(newSecundum);
1533
            }
1534
            if (newSecundumDetail != null || !keepSecundumIfUndefined){
1535
                synRelation.setSecMicroReference(newSecundumDetail);
1536
            }
1537

    
1538
            //set result  //why is this needed? Seems wrong to me (AM 10.10.2016)
1539
            if (!synRelation.equals(oldSynonym)){
1540
                result.setError();
1541
            }
1542
        }
1543

    
1544
        result.addUpdatedObject(oldTaxon);
1545
        result.addUpdatedObject(newTaxon);
1546
        saveOrUpdate(oldTaxon);
1547
        saveOrUpdate(newTaxon);
1548

    
1549
        return result;
1550
    }
1551

    
1552
    @Override
1553
    public <T extends TaxonBase>List<UuidAndTitleCache<T>> getUuidAndTitleCache(Class<T> clazz, Integer limit, String pattern) {
1554

    
1555
        return dao.getUuidAndTitleCache(clazz, limit, pattern);
1556
    }
1557

    
1558
    @Override
1559
    public Pager<SearchResult<TaxonBase>> findByFullText(
1560
            Class<? extends TaxonBase> clazz, String queryString,
1561
            Classification classification, TaxonNode subtree, boolean includeUnpublished, List<Language> languages,
1562
            boolean highlightFragments, Integer pageSize, Integer pageNumber,
1563
            List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
1564

    
1565
        LuceneSearch luceneSearch = prepareFindByFullTextSearch(clazz, queryString, classification, subtree,
1566
                null, includeUnpublished, languages, highlightFragments, null);
1567

    
1568
        // --- execute search
1569
        TopGroups<BytesRef> topDocsResultSet;
1570
        try {
1571
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
1572
        } catch (ParseException e) {
1573
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
1574
            luceneParseException.setStackTrace(e.getStackTrace());
1575
            throw luceneParseException;
1576
        }
1577

    
1578
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1579
        idFieldMap.put(CdmBaseType.TAXON_BASE, "id");
1580

    
1581
        // ---  initialize taxa, thighlight matches ....
1582
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1583
        @SuppressWarnings("rawtypes")
1584
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1585
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1586

    
1587
        long totalHits = topDocsResultSet != null ? Long.valueOf(topDocsResultSet.totalGroupCount) : 0;
1588
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
1589
    }
1590

    
1591
    @Transactional(readOnly = true)
1592
    @Override
1593
    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) {
1594
         long numberOfResults = dao.countByTitleWithRestrictions(clazz, queryString, matchmode, restrictions);
1595

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

    
1600
             results = dao.findByTitleWithRestrictions(clazz, queryString, matchmode, restrictions, pageSize, pageNumber, orderHints, propertyPaths);
1601
             results.addAll(dao.findByTitleWithRestrictions(clazz, "?".concat(queryString), matchmode, restrictions, pageSize, pageNumber, orderHints, propertyPaths));
1602
         }
1603
         Collections.sort(results, new TaxonComparator());
1604
         return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
1605
    }
1606

    
1607
    @Transactional(readOnly = true)
1608
    @Override
1609
    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) {
1610
        long numberOfResults = dao.countByTitle(clazz, queryString, matchmode, criteria);
1611
        //check whether there are doubtful taxa matching
1612
        long numberOfResults_doubtful = dao.countByTitle(clazz, "?".concat(queryString), matchmode, criteria);
1613
        List<S> results = new ArrayList<>();
1614
        if(numberOfResults > 0 || numberOfResults_doubtful > 0) { // no point checking again //TODO use AbstractPagerImpl.hasResultsInRange(numberOfResults, pageNumber, pageSize)
1615
               if (numberOfResults > 0){
1616
                   results = dao.findByTitle(clazz, queryString, matchmode, criteria, pageSize, pageNumber, orderHints, propertyPaths);
1617
               }else{
1618
                   results = new ArrayList<>();
1619
               }
1620
               if (numberOfResults_doubtful > 0){
1621
                   results.addAll(dao.findByTitle(clazz, "?".concat(queryString), matchmode,  criteria, pageSize, pageNumber, orderHints, propertyPaths));
1622
               }
1623
        }
1624
        Collections.sort(results, new TaxonComparator());
1625
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
1626
    }
1627

    
1628
    @Override
1629
    public Pager<SearchResult<TaxonBase>> findByDistribution(List<NamedArea> areaFilter, List<PresenceAbsenceTerm> statusFilter,
1630
            Classification classification, TaxonNode subtree,
1631
            Integer pageSize, Integer pageNumber,
1632
            List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
1633

    
1634
        LuceneSearch luceneSearch = prepareByDistributionSearch(areaFilter, statusFilter, classification, subtree);
1635

    
1636
        // --- execute search
1637
        TopGroups<BytesRef> topDocsResultSet;
1638
        try {
1639
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
1640
        } catch (ParseException e) {
1641
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
1642
            luceneParseException.setStackTrace(e.getStackTrace());
1643
            throw luceneParseException;
1644
        }
1645

    
1646
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1647
        idFieldMap.put(CdmBaseType.TAXON_BASE, "id");
1648

    
1649
        // ---  initialize taxa, thighlight matches ....
1650
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1651
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1652
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1653

    
1654
        long totalHits = topDocsResultSet != null ? Long.valueOf(topDocsResultSet.totalGroupCount) : 0;
1655
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
1656
    }
1657

    
1658
    /**
1659
     * @param clazz
1660
     * @param queryString
1661
     * @param classification
1662
     * @param includeUnpublished
1663
     * @param languages
1664
     * @param highlightFragments
1665
     * @param sortFields TODO
1666
     * @param directorySelectClass
1667
     * @return
1668
     */
1669
    protected LuceneSearch prepareFindByFullTextSearch(Class<? extends CdmBase> clazz, String queryString,
1670
            Classification classification, TaxonNode subtree, String className, boolean includeUnpublished, List<Language> languages,
1671
            boolean highlightFragments, SortField[] sortFields) {
1672

    
1673
        Builder finalQueryBuilder = new Builder();
1674
        Builder textQueryBuilder = new Builder();
1675

    
1676
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1677
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1678

    
1679
        if(sortFields == null){
1680
            sortFields = new SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING, false)};
1681
        }
1682
        luceneSearch.setSortFields(sortFields);
1683

    
1684
        // ---- search criteria
1685
        luceneSearch.setCdmTypRestriction(clazz);
1686

    
1687
        if(!StringUtils.isEmpty(queryString) && !queryString.equals("*") && !queryString.equals("?") ) {
1688
            textQueryBuilder.add(taxonBaseQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
1689
            textQueryBuilder.add(taxonBaseQueryFactory.newDefinedTermQuery("name.rank", queryString, languages), Occur.SHOULD);
1690
        }
1691
        if(className != null){
1692
            textQueryBuilder.add(taxonBaseQueryFactory.newTermQuery("classInfo.name", className, false), Occur.MUST);
1693
        }
1694

    
1695
        BooleanQuery textQuery = textQueryBuilder.build();
1696
        if(textQuery.clauses().size() > 0) {
1697
            finalQueryBuilder.add(textQuery, Occur.MUST);
1698
        }
1699

    
1700
        if(classification != null){
1701
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery(AcceptedTaxonBridge.DOC_KEY_CLASSIFICATION_ID, classification), Occur.MUST);
1702
        }
1703
        if(subtree != null){
1704
            finalQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(AcceptedTaxonBridge.DOC_KEY_TREEINDEX, subtree.treeIndexWc(), true), Occur.MUST);
1705
        }
1706
        if(!includeUnpublished)  {
1707
            String accPublishParam = TaxonBase.ACC_TAXON_BRIDGE_PREFIX + AcceptedTaxonBridge.DOC_KEY_PUBLISH_SUFFIX;
1708
            finalQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery(accPublishParam, true), Occur.MUST);
1709
            finalQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery("publish", true), Occur.MUST);
1710
        }
1711

    
1712
        luceneSearch.setQuery(finalQueryBuilder.build());
1713

    
1714
        if(highlightFragments){
1715
            luceneSearch.setHighlightFields(taxonBaseQueryFactory.getTextFieldNamesAsArray());
1716
        }
1717
        return luceneSearch;
1718
    }
1719

    
1720
    /**
1721
     * Uses org.apache.lucene.search.join.JoinUtil for query time joining, alternatively
1722
     * the BlockJoinQuery could be used. The latter might be more memory save but has the
1723
     * drawback of requiring to do the join an indexing time.
1724
     * see  http://dev.e-taxonomy.eu/trac/wiki/LuceneNotes#JoinsinLucene for more information on this.
1725
     *
1726
     * Joins TaxonRelationShip with Taxon depending on the direction of the given edge:
1727
     * <ul>
1728
     * <li>direct, everted: {@link Direction.relatedTo}: TaxonRelationShip.relatedTo.id --&gt; Taxon.id </li>
1729
     * <li>inverse: {@link Direction.relatedFrom}:  TaxonRelationShip.relatedFrom.id --&gt; Taxon.id </li>
1730
     * <ul>
1731
     * @param queryString
1732
     * @param classification
1733
     * @param languages
1734
     * @param highlightFragments
1735
     * @param sortFields TODO
1736
     *
1737
     * @return
1738
     * @throws IOException
1739
     */
1740
    protected LuceneSearch prepareFindByTaxonRelationFullTextSearch(TaxonRelationshipEdge edge, String queryString,
1741
            Classification classification, TaxonNode subtree, boolean includeUnpublished, List<Language> languages,
1742
            boolean highlightFragments, SortField[] sortFields) throws IOException {
1743

    
1744
        String fromField;
1745
        String queryTermField;
1746
        String toField = "id"; // TaxonBase.uuid
1747
        String publishField;
1748
        String publishFieldInvers;
1749

    
1750
        if(edge.isBidirectional()){
1751
            throw new RuntimeException("Bidirectional joining not supported!");
1752
        }
1753
        if(edge.isEvers()){
1754
            fromField = "relatedFrom.id";
1755
            queryTermField = "relatedFrom.titleCache";
1756
            publishField = "relatedFrom.publish";
1757
            publishFieldInvers = "relatedTo.publish";
1758
        } else if(edge.isInvers()) {
1759
            fromField = "relatedTo.id";
1760
            queryTermField = "relatedTo.titleCache";
1761
            publishField = "relatedTo.publish";
1762
            publishFieldInvers = "relatedFrom.publish";
1763
        } else {
1764
            throw new RuntimeException("Invalid direction: " + edge.getDirections());
1765
        }
1766

    
1767
        Builder finalQueryBuilder = new Builder();
1768

    
1769
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1770
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1771

    
1772
        Builder joinFromQueryBuilder = new Builder();
1773
        if(!StringUtils.isEmpty(queryString)){
1774
            joinFromQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(queryTermField, queryString), Occur.MUST);
1775
        }
1776
        joinFromQueryBuilder.add(taxonBaseQueryFactory.newEntityIdsQuery("type.id", edge.getRelationshipTypes()), Occur.MUST);
1777
        if(!includeUnpublished){
1778
            joinFromQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery(publishField, true), Occur.MUST);
1779
            joinFromQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery(publishFieldInvers, true), Occur.MUST);
1780
        }
1781

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

    
1784
        if(sortFields == null){
1785
            sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING,  false)};
1786
        }
1787
        luceneSearch.setSortFields(sortFields);
1788

    
1789
        finalQueryBuilder.add(joinQuery, Occur.MUST);
1790

    
1791
        if(classification != null){
1792
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery(AcceptedTaxonBridge.DOC_KEY_CLASSIFICATION_ID, classification), Occur.MUST);
1793
        }
1794
        if(subtree != null){
1795
            finalQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(AcceptedTaxonBridge.DOC_KEY_TREEINDEX, subtree.treeIndexWc(), true), Occur.MUST);
1796
        }
1797

    
1798
        luceneSearch.setQuery(finalQueryBuilder.build());
1799

    
1800
        if(highlightFragments){
1801
            luceneSearch.setHighlightFields(taxonBaseQueryFactory.getTextFieldNamesAsArray());
1802
        }
1803
        return luceneSearch;
1804
    }
1805

    
1806
    @Override
1807
    public Pager<SearchResult<TaxonBase>> findTaxaAndNamesByFullText(
1808
            EnumSet<TaxaAndNamesSearchMode> searchModes, String queryString,
1809
            Classification classification, TaxonNode subtree,
1810
            Set<NamedArea> namedAreas, Set<PresenceAbsenceTerm> distributionStatus, List<Language> languages,
1811
            boolean highlightFragments, Integer pageSize,
1812
            Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths)
1813
            throws IOException, LuceneParseException, LuceneMultiSearchException {
1814

    
1815
        // FIXME: allow taxonomic ordering
1816
        //  hql equivalent:  order by t.name.genusOrUninomial, case when t.name.specificEpithet like '\"%\"' then 1 else 0 end, t.name.specificEpithet, t.name.rank desc, t.name.nameCache";
1817
        // this require building a special sort column by a special classBridge
1818
        if(highlightFragments){
1819
            logger.warn("findTaxaAndNamesByFullText() : fragment highlighting is " +
1820
                    "currently not fully supported by this method and thus " +
1821
                    "may not work with common names and misapplied names.");
1822
        }
1823

    
1824
        // convert sets to lists
1825
        List<NamedArea> namedAreaList = null;
1826
        List<PresenceAbsenceTerm> distributionStatusList = null;
1827
        if(namedAreas != null){
1828
            namedAreaList = new ArrayList<>(namedAreas.size());
1829
            namedAreaList.addAll(namedAreas);
1830
        }
1831
        if(distributionStatus != null){
1832
            distributionStatusList = new ArrayList<>(distributionStatus.size());
1833
            distributionStatusList.addAll(distributionStatus);
1834
        }
1835

    
1836
        // set default if parameter is null
1837
        if(searchModes == null){
1838
            searchModes = EnumSet.of(TaxaAndNamesSearchMode.doTaxa);
1839
        }
1840

    
1841
        // set sort order and thus override any sort orders which may have been
1842
        // defined by prepare*Search methods
1843
        if(orderHints == null){
1844
            orderHints = OrderHint.NOMENCLATURAL_SORT_ORDER.asList();
1845
        }
1846
        SortField[] sortFields = new SortField[orderHints.size()];
1847
        int i = 0;
1848
        for(OrderHint oh : orderHints){
1849
            sortFields[i++] = oh.toSortField();
1850
        }
1851
//        SortField[] sortFields = new SortField[]{SortField.FIELD_SCORE, new SortField("id", SortField.STRING, false)};
1852
//        SortField[] sortFields = new SortField[]{new SortField(NomenclaturalSortOrderBrigde.NAME_SORT_FIELD_NAME, SortField.STRING, false)};
1853

    
1854
        boolean addDistributionFilter = namedAreas != null && namedAreas.size() > 0;
1855

    
1856
        List<LuceneSearch> luceneSearches = new ArrayList<>();
1857
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1858

    
1859
        /*
1860
          ======== filtering by distribution , HOWTO ========
1861

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

    
1867

    
1868
          3. how does it work in spatial?
1869
          see
1870
           - http://www.nsshutdown.com/projects/lucene/whitepaper/locallucene_v2.html
1871
           - http://www.infoq.com/articles/LuceneSpatialSupport
1872
           - http://www.mhaller.de/archives/156-Spatial-search-with-Lucene.html
1873
          ------------------------------------------------------------------------
1874

    
1875
          filter strategies:
1876
          A) use a separate distribution filter per index sub-query/search:
1877
           - byTaxonSyonym (query TaxaonBase):
1878
               use a join area filter (Distribution -> TaxonBase)
1879
           - byCommonName (query DescriptionElementBase): use an area filter on
1880
               DescriptionElementBase !!! PROBLEM !!!
1881
               This cannot work since the distributions are different entities than the
1882
               common names and thus these are different lucene documents.
1883
           - byMisaplliedNames (join query TaxonRelationship -> TaxonBase):
1884
               use a join area filter (Distribution -> TaxonBase)
1885

    
1886
          B) use a common distribution filter for all index sub-query/searches:
1887
           - use a common join area filter (Distribution -> TaxonBase)
1888
           - also implement the byCommonName as join query (CommonName -> TaxonBase)
1889
           PROBLEM in this case: we are losing the fragment highlighting for the
1890
           common names, since the returned documents are always TaxonBases
1891
        */
1892

    
1893
        /* The QueryFactory for creating filter queries on Distributions should
1894
         * The query factory used for the common names query cannot be reused
1895
         * for this case, since we want to only record the text fields which are
1896
         * actually used in the primary query
1897
         */
1898
        QueryFactory distributionFilterQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Distribution.class);
1899

    
1900
        Builder multiIndexByAreaFilterBuilder = new Builder();
1901
        boolean includeUnpublished = searchModes.contains(TaxaAndNamesSearchMode.includeUnpublished);
1902

    
1903
        // search for taxa or synonyms
1904
        if(searchModes.contains(TaxaAndNamesSearchMode.doTaxa) || searchModes.contains(TaxaAndNamesSearchMode.doSynonyms) ) {
1905
            @SuppressWarnings("rawtypes")
1906
            Class<? extends TaxonBase> taxonBaseSubclass = TaxonBase.class;
1907
            String className = null;
1908
            if(searchModes.contains(TaxaAndNamesSearchMode.doTaxa) && !searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
1909
                taxonBaseSubclass = Taxon.class;
1910
            } else if (!searchModes.contains(TaxaAndNamesSearchMode.doTaxa) && searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)) {
1911
                className = "eu.etaxonomy.cdm.model.taxon.Synonym";
1912
            }
1913
            luceneSearches.add(prepareFindByFullTextSearch(taxonBaseSubclass,
1914
                    queryString, classification, subtree, className,
1915
                    includeUnpublished, languages, highlightFragments, sortFields));
1916
            idFieldMap.put(CdmBaseType.TAXON_BASE, "id");
1917
            /* A) does not work!!!!
1918
            if(addDistributionFilter){
1919
                // in this case we need a filter which uses a join query
1920
                // to get the TaxonBase documents for the DescriptionElementBase documents
1921
                // which are matching the areas in question
1922
                Query taxonAreaJoinQuery = createByDistributionJoinQuery(
1923
                        namedAreaList,
1924
                        distributionStatusList,
1925
                        distributionFilterQueryFactory
1926
                        );
1927
                multiIndexByAreaFilter.add(new QueryWrapperFilter(taxonAreaJoinQuery), Occur.SHOULD);
1928
            }
1929
            */
1930
            if(addDistributionFilter && searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
1931
                // add additional area filter for synonyms
1932
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1933
                String toField = "accTaxon" + AcceptedTaxonBridge.DOC_KEY_ID_SUFFIX; // id in TaxonBase index
1934

    
1935
                //TODO replace by createByDistributionJoinQuery
1936
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
1937
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class, fromField, true, byDistributionQuery, toField, Taxon.class, ScoreMode.None);
1938
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1939

    
1940
            }
1941
        }
1942

    
1943
        // search by CommonTaxonName
1944
        if(searchModes.contains(TaxaAndNamesSearchMode.doTaxaByCommonNames)) {
1945
            // B)
1946
            QueryFactory descriptionElementQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(DescriptionElementBase.class);
1947
            Query byCommonNameJoinQuery = descriptionElementQueryFactory.newJoinQuery(
1948
                    CommonTaxonName.class,
1949
                    "inDescription.taxon.id",
1950
                    true,
1951
                    QueryFactory.addTypeRestriction(
1952
                                createByDescriptionElementFullTextQuery(queryString, classification, subtree, null, languages, descriptionElementQueryFactory)
1953
                                , CommonTaxonName.class
1954
                                ).build(), "id", null, ScoreMode.Max);
1955
            if (logger.isDebugEnabled()){logger.debug("byCommonNameJoinQuery: " + byCommonNameJoinQuery.toString());}
1956
            LuceneSearch byCommonNameSearch = new LuceneSearch(luceneIndexToolProvider,
1957
                    GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
1958
            byCommonNameSearch.setCdmTypRestriction(Taxon.class);
1959
            Builder builder = new BooleanQuery.Builder();
1960
            builder.add(byCommonNameJoinQuery, Occur.MUST);
1961
            if(!includeUnpublished)  {
1962
                QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1963
                builder.add(taxonBaseQueryFactory.newBooleanQuery("publish", true), Occur.MUST);
1964
            }
1965
            byCommonNameSearch.setQuery(builder.build());
1966
            byCommonNameSearch.setSortFields(sortFields);
1967

    
1968
            idFieldMap.put(CdmBaseType.TAXON_BASE, "id");
1969

    
1970
            luceneSearches.add(byCommonNameSearch);
1971

    
1972
            /* A) does not work!!!!
1973
            luceneSearches.add(
1974
                    prepareByDescriptionElementFullTextSearch(CommonTaxonName.class,
1975
                            queryString, classification, null, languages, highlightFragments)
1976
                        );
1977
            idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
1978
            if(addDistributionFilter){
1979
                // in this case we are able to use DescriptionElementBase documents
1980
                // which are matching the areas in question directly
1981
                BooleanQuery byDistributionQuery = createByDistributionQuery(
1982
                        namedAreaList,
1983
                        distributionStatusList,
1984
                        distributionFilterQueryFactory
1985
                        );
1986
                multiIndexByAreaFilter.add(new QueryWrapperFilter(byDistributionQuery), Occur.SHOULD);
1987
            } */
1988
        }
1989

    
1990

    
1991
        // search by misapplied names
1992
        //TODO merge with pro parte synonym search once #7487 is fixed
1993
        if(searchModes.contains(TaxaAndNamesSearchMode.doMisappliedNames) /*|| searchModes.contains(TaxaAndNamesSearchMode.doSynonyms) */) {
1994
            // NOTE:
1995
            // prepareFindByTaxonRelationFullTextSearch() is making use of JoinUtil.createJoinQuery()
1996
            // which allows doing query time joins
1997
            // finds the misapplied name (Taxon B) which is an misapplication for
1998
            // a related Taxon A.
1999
            //
2000
            Set<TaxonRelationshipType> relTypes = new HashSet<>();
2001
            if (searchModes.contains(TaxaAndNamesSearchMode.doMisappliedNames)){
2002
                relTypes.addAll(TaxonRelationshipType.allMisappliedNameTypes());
2003
            }
2004
//            if (searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
2005
//                relTypes.addAll(TaxonRelationshipType.allSynonymTypes());
2006
//            }
2007

    
2008
            luceneSearches.add(prepareFindByTaxonRelationFullTextSearch(
2009
                    new TaxonRelationshipEdge(relTypes, Direction.relatedTo),
2010
                    queryString, classification, subtree, includeUnpublished, languages, highlightFragments, sortFields));
2011
            idFieldMap.put(CdmBaseType.TAXON_BASE, "id");
2012

    
2013
            if(addDistributionFilter){
2014
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
2015

    
2016
                /*
2017
                 * Here I was facing a weird and nasty bug which took me bugging be really for hours until I found this solution.
2018
                 * Maybe this is a bug in java itself.
2019
                 *
2020
                 * When the string toField is constructed by using the expression TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString()
2021
                 * directly:
2022
                 *
2023
                 *    String toField = "relation." + TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString() +".to.id";
2024
                 *
2025
                 * The byDistributionQuery fails, however when the uuid is first stored in another string variable the query
2026
                 * will execute as expected:
2027
                 *
2028
                 *    String misappliedNameForUuid = TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString();
2029
                 *    String toField = "relation." + misappliedNameForUuid +".to.id";
2030
                 *
2031
                 * Comparing both strings by the String.equals method returns true, so both String are identical.
2032
                 *
2033
                 * The bug occurs when running eu.etaxonomy.cdm.api.service.TaxonServiceSearchTest in eclipse and in maven and seems to to be
2034
                 * 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)
2035
                 * The bug is persistent after a reboot of the development computer.
2036
                 */
2037
//                String misappliedNameForUuid = TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString();
2038
//                String toField = "relation." + misappliedNameForUuid +".to.id";
2039
                String toField = "relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id";
2040
//                System.out.println("relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id".equals("relation." + misappliedNameForUuid +".to.id") ? " > identical" : " > different");
2041
//                System.out.println("relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id".equals("relation." + TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString() +".to.id") ? " > identical" : " > different");
2042

    
2043
                //TODO replace by createByDistributionJoinQuery
2044
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
2045
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class,
2046
                        fromField, true, byDistributionQuery, toField, null, ScoreMode.None);
2047

    
2048
//                debug code for bug described above
2049
                //does not compile anymore since changing from lucene 3.6.2 to lucene 4.10+
2050
//                DocIdSet filterMatchSet = filter.getDocIdSet(luceneIndexToolProvider.getIndexReaderFor(Taxon.class));
2051
//                System.err.println(DocIdBitSetPrinter.docsAsString(filterMatchSet, 100));
2052

    
2053
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
2054
            }
2055
        }
2056

    
2057
        // search by pro parte synonyms
2058
        if(searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)) {
2059
            //TODO merge with misapplied name search once #7487 is fixed
2060
            Set<TaxonRelationshipType> relTypes = new HashSet<>();
2061
            relTypes.addAll(TaxonRelationshipType.allSynonymTypes());
2062

    
2063
            luceneSearches.add(prepareFindByTaxonRelationFullTextSearch(
2064
                    new TaxonRelationshipEdge(relTypes, Direction.relatedTo),
2065
                    queryString, classification, subtree, includeUnpublished, languages, highlightFragments, sortFields));
2066
            idFieldMap.put(CdmBaseType.TAXON_BASE, "id");
2067

    
2068
            if(addDistributionFilter){
2069
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
2070
                String toField = "relation.8a896603-0fa3-44c6-9cd7-df2d8792e577.to.id";
2071
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
2072
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class,
2073
                        fromField, true, byDistributionQuery, toField, null, ScoreMode.None);
2074
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
2075
            }
2076
        }//end pro parte synonyms
2077

    
2078
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider,
2079
                luceneSearches.toArray(new LuceneSearch[luceneSearches.size()]));
2080

    
2081
        if(addDistributionFilter){
2082

    
2083
            // B)
2084
            // in this case we need a filter which uses a join query
2085
            // to get the TaxonBase documents for the DescriptionElementBase documents
2086
            // which are matching the areas in question
2087
            //
2088
            // for doTaxa, doByCommonName
2089
            Query taxonAreaJoinQuery = createByDistributionJoinQuery(
2090
                    namedAreaList,
2091
                    distributionStatusList,
2092
                    distributionFilterQueryFactory,
2093
                    Taxon.class, true
2094
                    );
2095
            multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
2096
        }
2097

    
2098
        if (addDistributionFilter){
2099
            multiSearch.setFilter(multiIndexByAreaFilterBuilder.build());
2100
        }
2101

    
2102

    
2103
        // --- execute search
2104
        TopGroups<BytesRef> topDocsResultSet;
2105
        try {
2106
            topDocsResultSet = multiSearch.executeSearch(pageSize, pageNumber);
2107
        } catch (ParseException e) {
2108
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2109
            luceneParseException.setStackTrace(e.getStackTrace());
2110
            throw luceneParseException;
2111
        }
2112

    
2113
        // --- initialize taxa, highlight matches ....
2114
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
2115

    
2116

    
2117
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2118
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2119

    
2120
        long totalHits = (topDocsResultSet != null) ? Long.valueOf(topDocsResultSet.totalGroupCount) : 0;
2121
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
2122
    }
2123

    
2124
    /**
2125
     * @param namedAreaList at least one area must be in the list
2126
     * @param distributionStatusList optional
2127
     * @param toType toType
2128
     *      Optional parameter. Only used for debugging to print the toType documents
2129
     * @param asFilter TODO
2130
     * @return
2131
     * @throws IOException
2132
     */
2133
    protected Query createByDistributionJoinQuery(
2134
            List<NamedArea> namedAreaList,
2135
            List<PresenceAbsenceTerm> distributionStatusList,
2136
            QueryFactory queryFactory, Class<? extends CdmBase> toType, boolean asFilter
2137
            ) throws IOException {
2138

    
2139
        String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
2140
        String toField = "id"; // id in toType usually this is the TaxonBase index
2141

    
2142
        BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, queryFactory);
2143

    
2144
        ScoreMode scoreMode = asFilter ?  ScoreMode.None : ScoreMode.Max;
2145

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

    
2148
        return taxonAreaJoinQuery;
2149
    }
2150

    
2151
    /**
2152
     * @param namedAreaList
2153
     * @param distributionStatusList
2154
     * @param queryFactory
2155
     * @return
2156
     */
2157
    private BooleanQuery createByDistributionQuery(List<NamedArea> namedAreaList,
2158
            List<PresenceAbsenceTerm> distributionStatusList, QueryFactory queryFactory) {
2159
        Builder areaQueryBuilder = new Builder();
2160
        // area field from Distribution
2161
        areaQueryBuilder.add(queryFactory.newEntityIdsQuery("area.id", namedAreaList), Occur.MUST);
2162

    
2163
        // status field from Distribution
2164
        if(distributionStatusList != null && distributionStatusList.size() > 0){
2165
            areaQueryBuilder.add(queryFactory.newEntityIdsQuery("status.id", distributionStatusList), Occur.MUST);
2166
        }
2167

    
2168
        BooleanQuery areaQuery = areaQueryBuilder.build();
2169
        logger.debug("createByDistributionQuery() query: " + areaQuery.toString());
2170
        return areaQuery;
2171
    }
2172

    
2173
    /**
2174
     * This method has been primarily created for testing the area join query but might
2175
     * also be useful in other situations
2176
     *
2177
     * @param namedAreaList
2178
     * @param distributionStatusList
2179
     * @param classification
2180
     * @param highlightFragments
2181
     * @return
2182
     * @throws IOException
2183
     */
2184
    protected LuceneSearch prepareByDistributionSearch(
2185
            List<NamedArea> namedAreaList, List<PresenceAbsenceTerm> distributionStatusList,
2186
            Classification classification, TaxonNode subtree) throws IOException {
2187

    
2188
        Builder finalQueryBuilder = new Builder();
2189

    
2190
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
2191

    
2192
        // FIXME is this query factory using the wrong type?
2193
        QueryFactory taxonQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Taxon.class);
2194

    
2195
        SortField[] sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING, false)};
2196
        luceneSearch.setSortFields(sortFields);
2197

    
2198

    
2199
        Query byAreaQuery = createByDistributionJoinQuery(namedAreaList, distributionStatusList, taxonQueryFactory, null, false);
2200

    
2201
        finalQueryBuilder.add(byAreaQuery, Occur.MUST);
2202

    
2203
        if(classification != null){
2204
            finalQueryBuilder.add(taxonQueryFactory.newEntityIdQuery(AcceptedTaxonBridge.DOC_KEY_CLASSIFICATION_ID, classification), Occur.MUST);
2205
        }
2206
        if(subtree != null){
2207
            finalQueryBuilder.add(taxonQueryFactory.newTermQuery(AcceptedTaxonBridge.DOC_KEY_TREEINDEX, subtree.treeIndexWc(), true), Occur.MUST);
2208
        }
2209
        BooleanQuery finalQuery = finalQueryBuilder.build();
2210
        logger.info("prepareByAreaSearch() query: " + finalQuery.toString());
2211
        luceneSearch.setQuery(finalQuery);
2212

    
2213
        return luceneSearch;
2214
    }
2215

    
2216
    @Override
2217
    public Pager<SearchResult<TaxonBase>> findByDescriptionElementFullText(
2218
            Class<? extends DescriptionElementBase> clazz, String queryString,
2219
            Classification classification, TaxonNode subtree, List<Feature> features, List<Language> languages,
2220
            boolean highlightFragments, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
2221

    
2222
        LuceneSearch luceneSearch = prepareByDescriptionElementFullTextSearch(clazz, queryString, classification, subtree, features, languages, highlightFragments);
2223

    
2224
        // --- execute search
2225
        TopGroups<BytesRef> topDocsResultSet;
2226
        try {
2227
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
2228
        } catch (ParseException e) {
2229
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2230
            luceneParseException.setStackTrace(e.getStackTrace());
2231
            throw luceneParseException;
2232
        }
2233

    
2234
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
2235
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2236

    
2237
        // --- initialize taxa, highlight matches ....
2238
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
2239
        @SuppressWarnings("rawtypes")
2240
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2241
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2242

    
2243
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
2244
        return new DefaultPagerImpl<>(pageNumber, Long.valueOf(totalHits), pageSize, searchResults);
2245
    }
2246

    
2247
    @Override
2248
    public Pager<SearchResult<TaxonBase>> findByEverythingFullText(String queryString,
2249
            Classification classification, TaxonNode subtree, boolean includeUnpublished, List<Language> languages, boolean highlightFragments,
2250
            Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException, LuceneMultiSearchException {
2251

    
2252
        LuceneSearch luceneSearchByDescriptionElement = prepareByDescriptionElementFullTextSearch(null, queryString,
2253
                classification, subtree,
2254
                null, languages, highlightFragments);
2255
        LuceneSearch luceneSearchByTaxonBase = prepareFindByFullTextSearch(null, queryString, classification, subtree, null,
2256
                includeUnpublished, languages, highlightFragments, null);
2257

    
2258
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider, luceneSearchByDescriptionElement, luceneSearchByTaxonBase);
2259

    
2260
        // --- execute search
2261
        TopGroups<BytesRef> topDocsResultSet;
2262
        try {
2263
            topDocsResultSet = multiSearch.executeSearch(pageSize, pageNumber);
2264
        } catch (ParseException e) {
2265
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2266
            luceneParseException.setStackTrace(e.getStackTrace());
2267
            throw luceneParseException;
2268
        }
2269

    
2270
        // --- initialize taxa, highlight matches ....
2271
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
2272

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

    
2277
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2278
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2279

    
2280
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
2281
        return new DefaultPagerImpl<>(pageNumber, Long.valueOf(totalHits), pageSize, searchResults);
2282
    }
2283

    
2284
    /**
2285
     * @param clazz
2286
     * @param queryString
2287
     * @param classification
2288
     * @param features
2289
     * @param languages
2290
     * @param highlightFragments
2291
     * @param directorySelectClass
2292
     * @return
2293
     */
2294
    protected LuceneSearch prepareByDescriptionElementFullTextSearch(Class<? extends CdmBase> clazz,
2295
            String queryString, Classification classification, TaxonNode subtree, List<Feature> features,
2296
            List<Language> languages, boolean highlightFragments) {
2297

    
2298
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, DescriptionElementBase.class);
2299
        QueryFactory descriptionElementQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(DescriptionElementBase.class);
2300

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

    
2303
        BooleanQuery finalQuery = createByDescriptionElementFullTextQuery(queryString, classification, subtree, features,
2304
                languages, descriptionElementQueryFactory);
2305

    
2306
        luceneSearch.setSortFields(sortFields);
2307
        luceneSearch.setCdmTypRestriction(clazz);
2308
        luceneSearch.setQuery(finalQuery);
2309
        if(highlightFragments){
2310
            luceneSearch.setHighlightFields(descriptionElementQueryFactory.getTextFieldNamesAsArray());
2311
        }
2312

    
2313
        return luceneSearch;
2314
    }
2315

    
2316
    /**
2317
     * @param queryString
2318
     * @param classification
2319
     * @param features
2320
     * @param languages
2321
     * @param descriptionElementQueryFactory
2322
     * @return
2323
     */
2324
    private BooleanQuery createByDescriptionElementFullTextQuery(String queryString,
2325
            Classification classification, TaxonNode subtree, List<Feature> features,
2326
            List<Language> languages, QueryFactory descriptionElementQueryFactory) {
2327

    
2328
        Builder finalQueryBuilder = new Builder();
2329
        Builder textQueryBuilder = new Builder();
2330

    
2331
        if(!StringUtils.isEmpty(queryString)){
2332

    
2333
            textQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
2334

    
2335
            // common name
2336
            Builder nameQueryBuilder = new Builder();
2337
            if(languages == null || languages.size() == 0){
2338
                nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2339
            } else {
2340
                Builder languageSubQueryBuilder = new Builder();
2341
                for(Language lang : languages){
2342
                    languageSubQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("language.uuid",  lang.getUuid().toString(), false), Occur.SHOULD);
2343
                }
2344
                nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2345
                nameQueryBuilder.add(languageSubQueryBuilder.build(), Occur.MUST);
2346
            }
2347
            textQueryBuilder.add(nameQueryBuilder.build(), Occur.SHOULD);
2348

    
2349

    
2350
            // text field from TextData
2351
            textQueryBuilder.add(descriptionElementQueryFactory.newMultilanguageTextQuery("text", queryString, languages), Occur.SHOULD);
2352

    
2353
            // --- TermBase fields - by representation ----
2354
            // state field from CategoricalData
2355
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.state", queryString, languages), Occur.SHOULD);
2356

    
2357
            // state field from CategoricalData
2358
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.modifyingText", queryString, languages), Occur.SHOULD);
2359

    
2360
            // area field from Distribution
2361
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("area", queryString, languages), Occur.SHOULD);
2362

    
2363
            // status field from Distribution
2364
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("status", queryString, languages), Occur.SHOULD);
2365

    
2366
            finalQueryBuilder.add(textQueryBuilder.build(), Occur.MUST);
2367

    
2368
        }
2369
        // --- classification ----
2370

    
2371

    
2372
        if(classification != null){
2373
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityIdQuery("inDescription.taxon.taxonNodes.classification.id", classification), Occur.MUST);
2374
        }
2375
        if(subtree != null){
2376
            finalQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("inDescription.taxon.taxonNodes.treeIndex", subtree.treeIndexWc(), true), Occur.MUST);
2377
        }
2378

    
2379
        // --- IdentifieableEntity fields - by uuid
2380
        if(features != null && features.size() > 0 ){
2381
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityUuidsQuery("feature.uuid", features), Occur.MUST);
2382
        }
2383

    
2384
        // the description must be associated with a taxon
2385
        finalQueryBuilder.add(descriptionElementQueryFactory.newIsNotNullQuery("inDescription.taxon.id"), Occur.MUST);
2386

    
2387
        BooleanQuery finalQuery = finalQueryBuilder.build();
2388
        logger.info("prepareByDescriptionElementFullTextSearch() query: " + finalQuery.toString());
2389
        return finalQuery;
2390
    }
2391

    
2392
    @Override
2393
    public List<Synonym> createInferredSynonyms(Taxon taxon, Classification classification, SynonymType type, boolean doWithMisappliedNames){
2394

    
2395
        List <Synonym> inferredSynonyms = new ArrayList<>();
2396
        List<Synonym> inferredSynonymsToBeRemoved = new ArrayList<>();
2397

    
2398
        Map <UUID, IZoologicalName> zooHashMap = new HashMap<>();
2399
        boolean includeUnpublished = INCLUDE_UNPUBLISHED;
2400

    
2401
        UUID nameUuid= taxon.getName().getUuid();
2402
        IZoologicalName taxonName = getZoologicalName(nameUuid, zooHashMap);
2403
        String epithetOfTaxon = null;
2404
        String infragenericEpithetOfTaxon = null;
2405
        String infraspecificEpithetOfTaxon = null;
2406
        if (taxonName.isSpecies()){
2407
             epithetOfTaxon= taxonName.getSpecificEpithet();
2408
        } else if (taxonName.isInfraGeneric()){
2409
            infragenericEpithetOfTaxon = taxonName.getInfraGenericEpithet();
2410
        } else if (taxonName.isInfraSpecific()){
2411
            infraspecificEpithetOfTaxon = taxonName.getInfraSpecificEpithet();
2412
        }
2413
        String genusOfTaxon = taxonName.getGenusOrUninomial();
2414
        Set<TaxonNode> nodes = taxon.getTaxonNodes();
2415
        List<String> taxonNames = new ArrayList<>();
2416

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

    
2421
            if (node.getClassification().equals(classification)){
2422
                if (!node.isTopmostNode()){
2423
                    TaxonNode parent = node.getParent();
2424
                    parent = CdmBase.deproxy(parent);
2425
                    TaxonName parentName =  parent.getTaxon().getName();
2426
                    IZoologicalName zooParentName = CdmBase.deproxy(parentName);
2427
                    Taxon parentTaxon = CdmBase.deproxy(parent.getTaxon());
2428

    
2429
                    //create inferred synonyms for species, subspecies
2430
                    if ((parentName.isGenus() || parentName.isSpecies() || parentName.getRank().equals(Rank.SUBGENUS())) ){
2431

    
2432
                        Synonym inferredEpithet = null;
2433
                        Synonym inferredGenus = null;
2434
                        Synonym potentialCombination = null;
2435

    
2436
                        List<String> propertyPaths = new ArrayList<>();
2437
                        propertyPaths.add("synonym");
2438
                        propertyPaths.add("synonym.name");
2439
                        List<OrderHint> orderHintsSynonyms = new ArrayList<>();
2440
                        orderHintsSynonyms.add(new OrderHint("titleCache", SortOrder.ASCENDING));
2441

    
2442
                        List<Synonym> synonyMsOfParent = dao.getSynonyms(parentTaxon, SynonymType.HETEROTYPIC_SYNONYM_OF(), null, null,orderHintsSynonyms,propertyPaths);
2443
                        List<Synonym> synonymsOfTaxon= dao.getSynonyms(taxon, SynonymType.HETEROTYPIC_SYNONYM_OF(),
2444
                                null, null,orderHintsSynonyms,propertyPaths);
2445

    
2446
                        List<TaxonRelationship> taxonRelListParent = new ArrayList<>();
2447
                        List<TaxonRelationship> taxonRelListTaxon = new ArrayList<>();
2448
                        if (doWithMisappliedNames){
2449
                            List<OrderHint> orderHintsMisapplied = new ArrayList<>();
2450
                            orderHintsMisapplied.add(new OrderHint("relatedFrom.titleCache", SortOrder.ASCENDING));
2451
                            taxonRelListParent = dao.getTaxonRelationships(parentTaxon, TaxonRelationshipType.MISAPPLIED_NAME_FOR(),
2452
                                    includeUnpublished, null, null, orderHintsMisapplied, propertyPaths, Direction.relatedTo);
2453
                            taxonRelListTaxon = dao.getTaxonRelationships(taxon, TaxonRelationshipType.MISAPPLIED_NAME_FOR(),
2454
                                    includeUnpublished, null, null, orderHintsMisapplied, propertyPaths, Direction.relatedTo);
2455
                        }
2456

    
2457
                        if (type.equals(SynonymType.INFERRED_EPITHET_OF())){
2458
                            for (Synonym synonymRelationOfParent:synonyMsOfParent){
2459

    
2460
                                inferredEpithet = createInferredEpithets(taxon,
2461
                                        zooHashMap, taxonName, epithetOfTaxon,
2462
                                        infragenericEpithetOfTaxon,
2463
                                        infraspecificEpithetOfTaxon,
2464
                                        taxonNames, parentName,
2465
                                        synonymRelationOfParent);
2466

    
2467
                                inferredSynonyms.add(inferredEpithet);
2468
                                zooHashMap.put(inferredEpithet.getName().getUuid(), inferredEpithet.getName());
2469
                                taxonNames.add(inferredEpithet.getName().getNameCache());
2470
                            }
2471

    
2472
                            if (doWithMisappliedNames){
2473

    
2474
                                for (TaxonRelationship taxonRelationship: taxonRelListParent){
2475
                                     Taxon misappliedName = taxonRelationship.getFromTaxon();
2476

    
2477
                                     inferredEpithet = createInferredEpithets(taxon,
2478
                                             zooHashMap, taxonName, epithetOfTaxon,
2479
                                             infragenericEpithetOfTaxon,
2480
                                             infraspecificEpithetOfTaxon,
2481
                                             taxonNames, parentName,
2482
                                             misappliedName);
2483

    
2484
                                    inferredSynonyms.add(inferredEpithet);
2485
                                    zooHashMap.put(inferredEpithet.getName().getUuid(), inferredEpithet.getName());
2486
                                    taxonNames.add(inferredEpithet.getName().getNameCache());
2487
                                }
2488
                            }
2489

    
2490
                            if (!taxonNames.isEmpty()){
2491
                                List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2492
                                if (!synNotInCDM.isEmpty()){
2493
                                    inferredSynonymsToBeRemoved.clear();
2494

    
2495
                                    for (Synonym syn :inferredSynonyms){
2496
                                        IZoologicalName name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2497
                                        if (!synNotInCDM.contains(name.getNameCache())){
2498
                                            inferredSynonymsToBeRemoved.add(syn);
2499
                                        }
2500
                                    }
2501

    
2502
                                    // Remove identified Synonyms from inferredSynonyms
2503
                                    for (Synonym synonym : inferredSynonymsToBeRemoved) {
2504
                                        inferredSynonyms.remove(synonym);
2505
                                    }
2506
                                }
2507
                            }
2508

    
2509
                        }else if (type.equals(SynonymType.INFERRED_GENUS_OF())){
2510

    
2511
                            for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2512

    
2513
                                inferredGenus = createInferredGenus(taxon,
2514
                                        zooHashMap, taxonName, epithetOfTaxon,
2515
                                        genusOfTaxon, taxonNames, zooParentName, synonymRelationOfTaxon);
2516

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

    
2522
                            if (doWithMisappliedNames){
2523

    
2524
                                for (TaxonRelationship taxonRelationship: taxonRelListTaxon){
2525
                                    Taxon misappliedName = taxonRelationship.getFromTaxon();
2526
                                    inferredGenus = createInferredGenus(taxon, zooHashMap, taxonName, infraspecificEpithetOfTaxon, genusOfTaxon, taxonNames, zooParentName,  misappliedName);
2527

    
2528
                                    inferredSynonyms.add(inferredGenus);
2529
                                    zooHashMap.put(inferredGenus.getName().getUuid(), inferredGenus.getName());
2530
                                     taxonNames.add(inferredGenus.getName().getNameCache());
2531
                                }
2532
                            }
2533

    
2534

    
2535
                            if (!taxonNames.isEmpty()){
2536
                                List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2537
                                IZoologicalName name;
2538
                                if (!synNotInCDM.isEmpty()){
2539
                                    inferredSynonymsToBeRemoved.clear();
2540

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

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

    
2555
                        }else if (type.equals(SynonymType.POTENTIAL_COMBINATION_OF())){
2556

    
2557
                            Reference sourceReference = null; // TODO: Determination of sourceReference is redundant
2558
                            //for all synonyms of the parent...
2559
                            for (Synonym synonymRelationOfParent:synonyMsOfParent){
2560
                                TaxonName synName;
2561
                                HibernateProxyHelper.deproxy(synonymRelationOfParent);
2562

    
2563
                                synName = synonymRelationOfParent.getName();
2564

    
2565
                                // Set the sourceReference
2566
                                sourceReference = synonymRelationOfParent.getSec();
2567

    
2568
                                // Determine the idInSource
2569
                                String idInSourceParent = getIdInSource(synonymRelationOfParent);
2570

    
2571
                                IZoologicalName parentSynZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2572
                                String synParentGenus = parentSynZooName.getGenusOrUninomial();
2573
                                String synParentInfragenericName = null;
2574
                                String synParentSpecificEpithet = null;
2575

    
2576
                                if (parentSynZooName.isInfraGeneric()){
2577
                                    synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2578
                                }
2579
                                if (parentSynZooName.isSpecies()){
2580
                                    synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2581
                                }
2582

    
2583
                               /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2584
                                    synonymsGenus.put(synGenusName, idInSource);
2585
                                }*/
2586

    
2587
                                //for all synonyms of the taxon
2588

    
2589
                                for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2590

    
2591
                                    IZoologicalName zooSynName = getZoologicalName(synonymRelationOfTaxon.getName().getUuid(), zooHashMap);
2592
                                    potentialCombination = createPotentialCombination(idInSourceParent, parentSynZooName, zooSynName,
2593
                                            synParentGenus,
2594
                                            synParentInfragenericName,
2595
                                            synParentSpecificEpithet, synonymRelationOfTaxon, zooHashMap);
2596

    
2597
                                    taxon.addSynonym(potentialCombination, SynonymType.POTENTIAL_COMBINATION_OF());
2598
                                    inferredSynonyms.add(potentialCombination);
2599
                                    zooHashMap.put(potentialCombination.getName().getUuid(), potentialCombination.getName());
2600
                                     taxonNames.add(potentialCombination.getName().getNameCache());
2601
                                }
2602
                            }
2603

    
2604
                            if (doWithMisappliedNames){
2605

    
2606
                                for (TaxonRelationship parentRelationship: taxonRelListParent){
2607

    
2608
                                    TaxonName misappliedParentName;
2609

    
2610
                                    Taxon misappliedParent = parentRelationship.getFromTaxon();
2611
                                    misappliedParentName = misappliedParent.getName();
2612

    
2613
                                    HibernateProxyHelper.deproxy(misappliedParent);
2614

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

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

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

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

    
2633
                                    for (TaxonRelationship taxonRelationship: taxonRelListTaxon){
2634
                                        Taxon misappliedName = taxonRelationship.getFromTaxon();
2635
                                        IZoologicalName zooMisappliedName = getZoologicalName(misappliedName.getName().getUuid(), zooHashMap);
2636
                                        potentialCombination = createPotentialCombination(
2637
                                                idInSourceParent, parentSynZooName, zooMisappliedName,
2638
                                                synParentGenus,
2639
                                                synParentInfragenericName,
2640
                                                synParentSpecificEpithet, misappliedName, zooHashMap);
2641

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

    
2650
                            if (!taxonNames.isEmpty()){
2651
                                List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2652
                                IZoologicalName name;
2653
                                if (!synNotInCDM.isEmpty()){
2654
                                    inferredSynonymsToBeRemoved.clear();
2655
                                    for (Synonym syn :inferredSynonyms){
2656
                                        try{
2657
                                            name = syn.getName();
2658
                                        }catch (ClassCastException e){
2659
                                            name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2660
                                        }
2661
                                        if (!synNotInCDM.contains(name.getNameCache())){
2662
                                            inferredSynonymsToBeRemoved.add(syn);
2663
                                        }
2664
                                     }
2665
                                    // Remove identified Synonyms from inferredSynonyms
2666
                                    for (Synonym synonym : inferredSynonymsToBeRemoved) {
2667
                                        inferredSynonyms.remove(synonym);
2668
                                    }
2669
                                }
2670
                            }
2671
                        }
2672
                    }else {
2673
                        logger.info("The synonym type is not defined.");
2674
                        return inferredSynonyms;
2675
                    }
2676
                }
2677
            }
2678
        }
2679

    
2680
        return inferredSynonyms;
2681
    }
2682

    
2683
    private Synonym createPotentialCombination(String idInSourceParent,
2684
            IZoologicalName parentSynZooName, 	IZoologicalName zooSynName, String synParentGenus,
2685
            String synParentInfragenericName, String synParentSpecificEpithet,
2686
            TaxonBase<?> syn, Map<UUID, IZoologicalName> zooHashMap) {
2687
        Synonym potentialCombination;
2688
        Reference sourceReference;
2689
        IZoologicalName inferredSynName;
2690
        HibernateProxyHelper.deproxy(syn);
2691

    
2692
        // Set sourceReference
2693
        sourceReference = syn.getSec();
2694
        if (sourceReference == null){
2695
            logger.warn("The synonym has no sec reference because it is a misapplied name! Take the sec reference of taxon");
2696
            //TODO:Remove
2697
            if (!parentSynZooName.getTaxa().isEmpty()){
2698
                TaxonBase<?> taxon = parentSynZooName.getTaxa().iterator().next();
2699

    
2700
                sourceReference = taxon.getSec();
2701
            }
2702
        }
2703
        String synTaxonSpecificEpithet = zooSynName.getSpecificEpithet();
2704

    
2705
        String synTaxonInfraSpecificName= null;
2706

    
2707
        if (parentSynZooName.isSpecies()){
2708
            synTaxonInfraSpecificName = zooSynName.getInfraSpecificEpithet();
2709
        }
2710

    
2711
        /*if (epithetName != null && !synonymsEpithet.contains(epithetName)){
2712
            synonymsEpithet.add(epithetName);
2713
        }*/
2714

    
2715
        //create potential combinations...
2716
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(syn.getName().getRank());
2717

    
2718
        inferredSynName.setGenusOrUninomial(synParentGenus);
2719
        if (zooSynName.isSpecies()){
2720
              inferredSynName.setSpecificEpithet(synTaxonSpecificEpithet);
2721
              if (parentSynZooName.isInfraGeneric()){
2722
                  inferredSynName.setInfraGenericEpithet(synParentInfragenericName);
2723
              }
2724
        }
2725
        if (zooSynName.isInfraSpecific()){
2726
            inferredSynName.setSpecificEpithet(synParentSpecificEpithet);
2727
            inferredSynName.setInfraSpecificEpithet(synTaxonInfraSpecificName);
2728
        }
2729
        if (parentSynZooName.isInfraGeneric()){
2730
            inferredSynName.setInfraGenericEpithet(synParentInfragenericName);
2731
        }
2732

    
2733
        potentialCombination = Synonym.NewInstance(inferredSynName, null);
2734

    
2735
        // Set the sourceReference
2736
        potentialCombination.setSec(sourceReference);
2737

    
2738

    
2739
        // Determine the idInSource
2740
        String idInSourceSyn= getIdInSource(syn);
2741

    
2742
        if (idInSourceParent != null && idInSourceSyn != null) {
2743
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2744
            inferredSynName.addSource(originalSource);
2745
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2746
            potentialCombination.addSource(originalSource);
2747
        }
2748

    
2749
        return potentialCombination;
2750
    }
2751

    
2752
    private Synonym createInferredGenus(Taxon taxon,
2753
            Map<UUID, IZoologicalName> zooHashMap, IZoologicalName taxonName,
2754
            String epithetOfTaxon, String genusOfTaxon,
2755
            List<String> taxonNames, IZoologicalName zooParentName,
2756
            TaxonBase syn) {
2757

    
2758
        Synonym inferredGenus;
2759
        TaxonName synName;
2760
        IZoologicalName inferredSynName;
2761
        synName =syn.getName();
2762
        HibernateProxyHelper.deproxy(syn);
2763

    
2764
        // Determine the idInSource
2765
        String idInSourceSyn = getIdInSource(syn);
2766
        String idInSourceTaxon = getIdInSource(taxon);
2767
        // Determine the sourceReference
2768
        Reference sourceReference = syn.getSec();
2769

    
2770
        //logger.warn(sourceReference.getTitleCache());
2771

    
2772
        synName = syn.getName();
2773
        IZoologicalName synZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2774
        String synSpeciesEpithetName = synZooName.getSpecificEpithet();
2775
         /* if (synonymsEpithet != null && !synonymsEpithet.contains(synSpeciesEpithetName)){
2776
            synonymsEpithet.add(synSpeciesEpithetName);
2777
        }*/
2778

    
2779
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2780
        //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...
2781

    
2782
        inferredSynName.setGenusOrUninomial(genusOfTaxon);
2783
        if (zooParentName.isInfraGeneric()){
2784
            inferredSynName.setInfraGenericEpithet(zooParentName.getInfraGenericEpithet());
2785
        }
2786

    
2787
        if (taxonName.isSpecies()){
2788
            inferredSynName.setSpecificEpithet(synSpeciesEpithetName);
2789
        }
2790
        if (taxonName.isInfraSpecific()){
2791
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2792
            inferredSynName.setInfraSpecificEpithet(synZooName.getInfraGenericEpithet());
2793
        }
2794

    
2795
        inferredGenus = Synonym.NewInstance(inferredSynName, null);
2796

    
2797
        // Set the sourceReference
2798
        inferredGenus.setSec(sourceReference);
2799

    
2800
        // Add the original source
2801
        if (idInSourceSyn != null && idInSourceTaxon != null) {
2802
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2803
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2804
            inferredGenus.addSource(originalSource);
2805

    
2806
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2807
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2808
            inferredSynName.addSource(originalSource);
2809
            originalSource = null;
2810

    
2811
        }else{
2812
            logger.error("There is an idInSource missing: " + idInSourceSyn + " of Synonym or " + idInSourceTaxon + " of Taxon");
2813
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2814
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2815
            inferredGenus.addSource(originalSource);
2816

    
2817
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2818
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2819
            inferredSynName.addSource(originalSource);
2820
            originalSource = null;
2821
        }
2822

    
2823
        taxon.addSynonym(inferredGenus, SynonymType.INFERRED_GENUS_OF());
2824

    
2825
        return inferredGenus;
2826
    }
2827

    
2828
    private Synonym createInferredEpithets(Taxon taxon,
2829
            Map<UUID, IZoologicalName> zooHashMap, IZoologicalName taxonName,
2830
            String epithetOfTaxon, String infragenericEpithetOfTaxon,
2831
            String infraspecificEpithetOfTaxon, List<String> taxonNames,
2832
            TaxonName parentName, TaxonBase<?> syn) {
2833

    
2834
        Synonym inferredEpithet;
2835
        TaxonName synName;
2836
        IZoologicalName inferredSynName;
2837
        HibernateProxyHelper.deproxy(syn);
2838

    
2839
        // Determine the idInSource
2840
        String idInSourceSyn = getIdInSource(syn);
2841
        String idInSourceTaxon =  getIdInSource(taxon);
2842
        // Determine the sourceReference
2843
        Reference sourceReference = syn.getSec();
2844

    
2845
        if (sourceReference == null){
2846
             logger.warn("The synonym has no sec reference because it is a misapplied name! Take the sec reference of taxon" + taxon.getSec());
2847
             sourceReference = taxon.getSec();
2848
        }
2849

    
2850
        synName = syn.getName();
2851
        IZoologicalName zooSynName = getZoologicalName(synName.getUuid(), zooHashMap);
2852
        String synGenusName = zooSynName.getGenusOrUninomial();
2853
        String synInfraGenericEpithet = null;
2854
        String synSpecificEpithet = null;
2855

    
2856
        if (zooSynName.getInfraGenericEpithet() != null){
2857
            synInfraGenericEpithet = zooSynName.getInfraGenericEpithet();
2858
        }
2859

    
2860
        if (zooSynName.isInfraSpecific()){
2861
            synSpecificEpithet = zooSynName.getSpecificEpithet();
2862
        }
2863

    
2864
           /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2865
            synonymsGenus.put(synGenusName, idInSource);
2866
        }*/
2867

    
2868
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2869

    
2870
        // DEBUG TODO: for subgenus or subspecies the infrageneric or infraspecific epithet should be used!!!
2871
        if (epithetOfTaxon == null && infragenericEpithetOfTaxon == null && infraspecificEpithetOfTaxon == null) {
2872
            logger.error("This specificEpithet is NULL" + taxon.getTitleCache());
2873
        }
2874
        inferredSynName.setGenusOrUninomial(synGenusName);
2875

    
2876
        if (parentName.isInfraGeneric()){
2877
            inferredSynName.setInfraGenericEpithet(synInfraGenericEpithet);
2878
        }
2879
        if (taxonName.isSpecies()){
2880
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2881
        }else if (taxonName.isInfraSpecific()){
2882
            inferredSynName.setSpecificEpithet(synSpecificEpithet);
2883
            inferredSynName.setInfraSpecificEpithet(infraspecificEpithetOfTaxon);
2884
        }
2885

    
2886
        inferredEpithet = Synonym.NewInstance(inferredSynName, null);
2887

    
2888
        // Set the sourceReference
2889
        inferredEpithet.setSec(sourceReference);
2890

    
2891
        /* Add the original source
2892
        if (idInSource != null) {
2893
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(idInSource, "InferredEpithetOf", syn.getSec(), null);
2894

    
2895
            // Add the citation
2896
            Reference citation = getCitation(syn);
2897
            if (citation != null) {
2898
                originalSource.setCitation(citation);
2899
                inferredEpithet.addSource(originalSource);
2900
            }
2901
        }*/
2902
        String taxonId = idInSourceTaxon+ "; " + idInSourceSyn;
2903

    
2904

    
2905
        IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2906
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2907

    
2908
        inferredEpithet.addSource(originalSource);
2909

    
2910
        originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2911
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2912

    
2913
        inferredSynName.addSource(originalSource);
2914

    
2915
        taxon.addSynonym(inferredEpithet, SynonymType.INFERRED_EPITHET_OF());
2916

    
2917
        return inferredEpithet;
2918
    }
2919

    
2920
    /**
2921
     * Returns an existing IZoologicalName or extends an internal hashmap if it does not exist.
2922
     * Very likely only useful for createInferredSynonyms().
2923
     * @param uuid
2924
     * @param zooHashMap
2925
     * @return
2926
     */
2927
    private IZoologicalName getZoologicalName(UUID uuid, Map <UUID, IZoologicalName> zooHashMap) {
2928
        IZoologicalName taxonName =nameDao.findZoologicalNameByUUID(uuid);
2929
        if (taxonName == null) {
2930
            taxonName = zooHashMap.get(uuid);
2931
        }
2932
        return taxonName;
2933
    }
2934

    
2935
    /**
2936
     * Returns the idInSource for a given Synonym.
2937
     * @param syn
2938
     */
2939
    private String getIdInSource(TaxonBase<?> taxonBase) {
2940
        String idInSource = null;
2941
        Set<IdentifiableSource> sources = taxonBase.getSources();
2942
        if (sources.size() == 1) {
2943
            IdentifiableSource source = sources.iterator().next();
2944
            if (source != null) {
2945
                idInSource  = source.getIdInSource();
2946
            }
2947
        } else if (sources.size() > 1) {
2948
            int count = 1;
2949
            idInSource = "";
2950
            for (IdentifiableSource source : sources) {
2951
                idInSource += source.getIdInSource();
2952
                if (count < sources.size()) {
2953
                    idInSource += "; ";
2954
                }
2955
                count++;
2956
            }
2957
        } else if (sources.size() == 0){
2958
            logger.warn("No idInSource for TaxonBase " + taxonBase.getUuid() + " - " + taxonBase.getTitleCache());
2959
        }
2960

    
2961
        return idInSource;
2962
    }
2963

    
2964
    /**
2965
     * Returns the citation for a given Synonym.
2966
     * @param syn
2967
     */
2968
    private Reference getCitation(Synonym syn) {
2969
        Reference citation = null;
2970
        Set<IdentifiableSource> sources = syn.getSources();
2971
        if (sources.size() == 1) {
2972
            IdentifiableSource source = sources.iterator().next();
2973
            if (source != null) {
2974
                citation = source.getCitation();
2975
            }
2976
        } else if (sources.size() > 1) {
2977
            logger.warn("This Synonym has more than one source: " + syn.getUuid() + " (" + syn.getTitleCache() +")");
2978
        }
2979

    
2980
        return citation;
2981
    }
2982

    
2983
    @Override
2984
    public List<Synonym>  createAllInferredSynonyms(Taxon taxon, Classification tree, boolean doWithMisappliedNames){
2985
        List <Synonym> inferredSynonyms = new ArrayList<>();
2986

    
2987
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.INFERRED_EPITHET_OF(), doWithMisappliedNames));
2988
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.INFERRED_GENUS_OF(), doWithMisappliedNames));
2989
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.POTENTIAL_COMBINATION_OF(), doWithMisappliedNames));
2990

    
2991
        return inferredSynonyms;
2992
    }
2993

    
2994
    @Override
2995
    public List<Classification> listClassifications(TaxonBase taxonBase, Integer limit, Integer start, List<String> propertyPaths) {
2996

    
2997
        // TODO quickly implemented, create according dao !!!!
2998
        Set<TaxonNode> nodes = new HashSet<>();
2999
        Set<Classification> classifications = new HashSet<>();
3000
        List<Classification> list = new ArrayList<>();
3001

    
3002
        if (taxonBase == null) {
3003
            return list;
3004
        }
3005

    
3006
        taxonBase = load(taxonBase.getUuid());
3007

    
3008
        if (taxonBase instanceof Taxon) {
3009
            nodes.addAll(((Taxon)taxonBase).getTaxonNodes());
3010
        } else {
3011
            Taxon taxon = ((Synonym)taxonBase).getAcceptedTaxon();
3012
            if (taxon != null){
3013
                nodes.addAll(taxon.getTaxonNodes());
3014
            }
3015
        }
3016
        for (TaxonNode node : nodes) {
3017
            classifications.add(node.getClassification());
3018
        }
3019
        list.addAll(classifications);
3020
        return list;
3021
    }
3022

    
3023
    @Override
3024
    @Transactional(readOnly = false)
3025
    public UpdateResult changeRelatedTaxonToSynonym(UUID fromTaxonUuid,
3026
            UUID toTaxonUuid,
3027
            TaxonRelationshipType oldRelationshipType,
3028
            SynonymType synonymType) throws DataChangeNoRollbackException {
3029
        UpdateResult result = new UpdateResult();
3030
        Taxon fromTaxon = (Taxon) dao.load(fromTaxonUuid);
3031
        Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
3032
        result = changeRelatedTaxonToSynonym(fromTaxon, toTaxon, oldRelationshipType, synonymType);
3033

    
3034
        result.addUpdatedObject(toTaxon);
3035
        result.addUpdatedObject(result.getCdmEntity());
3036

    
3037
        return result;
3038
    }
3039

    
3040
    @Override
3041
    @Transactional(readOnly = false)
3042
    public UpdateResult changeRelatedTaxonToSynonym(Taxon fromTaxon, Taxon toTaxon, TaxonRelationshipType oldRelationshipType,
3043
            SynonymType synonymType) throws DataChangeNoRollbackException {
3044

    
3045
        UpdateResult result = new UpdateResult();
3046
        // Create new synonym using concept name
3047
        TaxonName synonymName = fromTaxon.getName();
3048

    
3049
        // Remove concept relation from taxon
3050
        toTaxon.removeTaxon(fromTaxon, oldRelationshipType);
3051

    
3052
        // Create a new synonym for the taxon
3053
        Synonym synonym;
3054
        if (synonymType != null
3055
                && synonymType.equals(SynonymType.HOMOTYPIC_SYNONYM_OF())){
3056
            synonym = Synonym.NewInstance(synonymName, fromTaxon.getSec());
3057
            toTaxon.addHomotypicSynonym(synonym);
3058
        } else{
3059
            synonym = toTaxon.addHeterotypicSynonymName(synonymName);
3060
        }
3061
        //keep the publish flag
3062
        synonym.setPublish(fromTaxon.isPublish());
3063
        this.saveOrUpdate(toTaxon);
3064
        //TODO: configurator and classification
3065
        TaxonDeletionConfigurator config = new TaxonDeletionConfigurator();
3066
        config.setDeleteNameIfPossible(false);
3067
        result.includeResult(this.deleteTaxon(fromTaxon.getUuid(), config, null));
3068
        result.setCdmEntity(synonym);
3069
        result.addUpdatedObject(toTaxon);
3070
        result.addUpdatedObject(synonym);
3071
        return result;
3072
    }
3073

    
3074
    @Override
3075
    public DeleteResult isDeletable(UUID taxonBaseUuid, DeleteConfiguratorBase config){
3076
        DeleteResult result = new DeleteResult();
3077
        TaxonBase<?> taxonBase = load(taxonBaseUuid);
3078
        Set<CdmBase> references = commonService.getReferencingObjectsForDeletion(taxonBase);
3079
        if (taxonBase instanceof Taxon){
3080
            TaxonDeletionConfigurator taxonConfig = (TaxonDeletionConfigurator) config;
3081
            List<String> propertyPaths = new ArrayList<>();
3082
            propertyPaths.add("taxonNodes");
3083
            Taxon taxon = (Taxon)load(taxonBaseUuid, propertyPaths);
3084

    
3085
            result = isDeletableForTaxon(references, taxonConfig );
3086

    
3087
            if (taxonConfig.isDeleteNameIfPossible()){
3088
                if (taxonBase.getName() != null){
3089
                    DeleteResult nameResult = nameService.isDeletable(taxonBase.getName().getUuid(), taxonConfig.getNameDeletionConfig(), taxon.getUuid());
3090
                    if (!nameResult.isOk()){
3091
                        result.addExceptions(nameResult.getExceptions());
3092
                    }
3093
                }
3094

    
3095
            }
3096
        }else{
3097
            SynonymDeletionConfigurator synonymConfig = (SynonymDeletionConfigurator) config;
3098
            result = isDeletableForSynonym(references, synonymConfig);
3099
            if (synonymConfig.isDeleteNameIfPossible() && taxonBase.getName() != null){
3100
                DeleteResult nameResult = nameService.isDeletable(taxonBase.getName().getUuid(), synonymConfig.getNameDeletionConfig(), taxonBase.getUuid());
3101
                if (!nameResult.isOk()){
3102
                    result.addExceptions(nameResult.getExceptions());
3103

    
3104
                }
3105
            }
3106
        }
3107

    
3108
        return result;
3109
    }
3110

    
3111
    private DeleteResult isDeletableForSynonym(Set<CdmBase> references, SynonymDeletionConfigurator config){
3112

    
3113
        DeleteResult result = new DeleteResult();
3114
        for (CdmBase ref: references){
3115
            if (!(ref instanceof Taxon || ref instanceof TaxonName || ref instanceof SecundumSource)){
3116
                String message = "The Synonym can't be deleted as long as it is referenced by " + ref.getClass().getSimpleName() + " with id "+ ref.getId();
3117
                result.addException(new ReferencedObjectUndeletableException(message));
3118
                result.addRelatedObject(ref);
3119
                result.setAbort();
3120
            }
3121
        }
3122

    
3123
        return result;
3124
    }
3125

    
3126
    private DeleteResult isDeletableForTaxon(Set<CdmBase> references, TaxonDeletionConfigurator config){
3127
        String message = null;
3128
        DeleteResult result = new DeleteResult();
3129
        for (CdmBase ref: references){
3130
            if (!(ref instanceof TaxonName || ref instanceof SecundumSource)){
3131
            	message = null;
3132
                if (!config.isDeleteSynonymRelations() && (ref instanceof Synonym)){
3133
                    message = "The taxon can't be deleted as long as it has synonyms.";
3134
                }
3135
                if (!config.isDeleteDescriptions() && (ref instanceof DescriptionBase)){
3136
                    message = "The taxon can't be deleted as long as it has factual data.";
3137
                }
3138

    
3139
                if (!config.isDeleteTaxonNodes() && (ref instanceof TaxonNode)){
3140
                    message = "The taxon can't be deleted as long as it belongs to a taxon node.";
3141
                }
3142
                if (ref instanceof TaxonNode && config.getClassificationUuid() != null && !config.isDeleteInAllClassifications() && !((TaxonNode)ref).getClassification().getUuid().equals(config.getClassificationUuid())){
3143
                    message = "The taxon can't be deleted as long as it is used in more than one classification";
3144

    
3145
                }
3146
                if (!config.isDeleteTaxonRelationships() && (ref instanceof TaxonRelationship)){
3147
                    if (!config.isDeleteMisappliedNames() &&
3148
                            (((TaxonRelationship)ref).getType().isMisappliedName())){
3149
                        message = "The taxon can't be deleted as long as it has misapplied names or invalid designations.";
3150
                    } else{
3151
                        message = "The taxon can't be deleted as long as it belongs to taxon relationship.";
3152
                    }
3153
                }
3154
                if (ref instanceof PolytomousKeyNode){
3155
                    message = "The taxon can't be deleted as long as it is referenced by a polytomous key node.";
3156
                }
3157

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

    
3162
               /* //PolytomousKeyNode
3163
                if (referencingObject.isInstanceOf(PolytomousKeyNode.class)){
3164
                    String message = "Taxon" + taxon.getTitleCache() + " can't be deleted as it is used in polytomous key node";
3165
                    return message;
3166
                }*/
3167

    
3168
                //TaxonInteraction
3169
                if (ref.isInstanceOf(TaxonInteraction.class)){
3170
                    message = "Taxon can't be deleted as it is used in taxonInteraction#taxon2";
3171
                }
3172

    
3173
              //TaxonInteraction
3174
                if (ref.isInstanceOf(DeterminationEvent.class)){
3175
                    message = "Taxon can't be deleted as it is used in a determination event";
3176
                }
3177
            }
3178
            if (message != null){
3179
	            result.addException(new ReferencedObjectUndeletableException(message));
3180
	            result.addRelatedObject(ref);
3181
	            result.setAbort();
3182
            }
3183
        }
3184

    
3185
        return result;
3186
    }
3187

    
3188
    @Override
3189
    public IncludedTaxaDTO listIncludedTaxa(UUID taxonUuid, IncludedTaxonConfiguration config) {
3190
        IncludedTaxaDTO result = new IncludedTaxaDTO(taxonUuid);
3191

    
3192
        //preliminary implementation
3193

    
3194
        Set<Taxon> taxa = new HashSet<>();
3195
        TaxonBase<?> taxonBase = find(taxonUuid);
3196
        if (taxonBase == null){
3197
            return new IncludedTaxaDTO();
3198
        }else if (taxonBase.isInstanceOf(Taxon.class)){
3199
            Taxon taxon = CdmBase.deproxy(taxonBase, Taxon.class);
3200
            taxa.add(taxon);
3201
        }else if (taxonBase.isInstanceOf(Synonym.class)){
3202
            //TODO partial synonyms ??
3203
            //TODO synonyms in general
3204
            Synonym syn = CdmBase.deproxy(taxonBase, Synonym.class);
3205
            taxa.add(syn.getAcceptedTaxon());
3206
        }else{
3207
            throw new IllegalArgumentException("Unhandled class " + taxonBase.getClass().getSimpleName());
3208
        }
3209

    
3210
        Set<Taxon> related = makeRelatedIncluded(taxa, result, config);
3211
        int i = 0;
3212
        while((! related.isEmpty()) && i++ < 100){  //to avoid
3213
             related = makeRelatedIncluded(related, result, config);
3214
        }
3215

    
3216
        return result;
3217
    }
3218

    
3219
    /**
3220
     * @param uncheckedTaxa
3221
     * @param existingTaxa
3222
     * @param config
3223
     *
3224
     * Computes all children and conceptually congruent and included taxa and adds them to the existingTaxa
3225
     * data structure.
3226
     * @return the set of conceptually related taxa for further use
3227
     */
3228
    private Set<Taxon> makeRelatedIncluded(Set<Taxon> uncheckedTaxa, IncludedTaxaDTO existingTaxa, IncludedTaxonConfiguration config) {
3229

    
3230
        //children
3231
        Set<TaxonNode> taxonNodes = new HashSet<>();
3232
        for (Taxon taxon: uncheckedTaxa){
3233
            taxonNodes.addAll(taxon.getTaxonNodes());
3234
        }
3235

    
3236
        Set<Taxon> children = new HashSet<>();
3237
        if (! config.onlyCongruent){
3238
            for (TaxonNode node: taxonNodes){
3239
                List<TaxonNode> childNodes = nodeService.loadChildNodesOfTaxonNode(node, null, true, config.includeUnpublished, null);
3240
                for (TaxonNode child : childNodes){
3241
                    children.add(child.getTaxon());
3242
                }
3243
            }
3244
            children.remove(null);  // just to be on the save side
3245
        }
3246

    
3247
        Iterator<Taxon> it = children.iterator();
3248
        while(it.hasNext()){
3249
            UUID uuid = it.next().getUuid();
3250
            if (existingTaxa.contains(uuid)){
3251
                it.remove();
3252
            }else{
3253
                existingTaxa.addIncludedTaxon(uuid, new ArrayList<>(), false);
3254
            }
3255
        }
3256

    
3257
        //concept relations
3258
        Set<Taxon> uncheckedAndChildren = new HashSet<>(uncheckedTaxa);
3259
        uncheckedAndChildren.addAll(children);
3260

    
3261
        Set<Taxon> relatedTaxa = makeConceptIncludedTaxa(uncheckedAndChildren, existingTaxa, config);
3262

    
3263

    
3264
        Set<Taxon> result = new HashSet<>(relatedTaxa);
3265
        return result;
3266
    }
3267

    
3268
    /**
3269
     * Computes all conceptually congruent or included taxa and adds them to the existingTaxa data structure.
3270
     * @return the set of these computed taxa
3271
     */
3272
    private Set<Taxon> makeConceptIncludedTaxa(Set<Taxon> unchecked, IncludedTaxaDTO existingTaxa, IncludedTaxonConfiguration config) {
3273
        Set<Taxon> result = new HashSet<>();
3274

    
3275
        for (Taxon taxon : unchecked){
3276
            Set<TaxonRelationship> fromRelations = taxon.getRelationsFromThisTaxon();
3277
            Set<TaxonRelationship> toRelations = taxon.getRelationsToThisTaxon();
3278

    
3279
            for (TaxonRelationship fromRel : fromRelations){
3280
                if (config.includeDoubtful == false && fromRel.isDoubtful()){
3281
                    continue;
3282
                }
3283
                TaxonRelationshipType fromRelType = fromRel.getType();
3284
                if (fromRelType.equals(TaxonRelationshipType.CONGRUENT_TO()) ||
3285
                        !config.onlyCongruent && (
3286
                            fromRelType.equals(TaxonRelationshipType.INCLUDES()) ||
3287
                            fromRelType.equals(TaxonRelationshipType.CONGRUENT_OR_INCLUDES())
3288
                        )
3289
                    ){
3290
                    result.add(fromRel.getToTaxon());
3291
                }
3292
            }
3293

    
3294
            for (TaxonRelationship toRel : toRelations){
3295
                if (config.includeDoubtful == false && toRel.isDoubtful()){
3296
                    continue;
3297
                }
3298
                TaxonRelationshipType fromRelType = toRel.getType();
3299
                if (fromRelType.equals(TaxonRelationshipType.CONGRUENT_TO()) ||
3300
                        !config.includeDoubtful && fromRelType.equals(TaxonRelationshipType.TAXONOMICALLY_INCLUDED_IN())){
3301
                    result.add(toRel.getFromTaxon());
3302
                }
3303
            }
3304
        }
3305

    
3306
        Iterator<Taxon> it = result.iterator();
3307
        while(it.hasNext()){
3308
            UUID uuid = it.next().getUuid();
3309
            if (existingTaxa.contains(uuid)){
3310
                it.remove();
3311
            }else{
3312
                existingTaxa.addIncludedTaxon(uuid, new ArrayList<>(), false);
3313
            }
3314
        }
3315
        return result;
3316
    }
3317

    
3318
    @Override
3319
    public List<TaxonBase> findTaxaByName(MatchingTaxonConfigurator config){
3320
        @SuppressWarnings("rawtypes")
3321
        List<TaxonBase> taxonList = dao.getTaxaByName(true, config.isIncludeSynonyms(), false, false, false,
3322
                config.getTaxonNameTitle(), null, null, MatchMode.EXACT, null, config.isIncludeSynonyms(), null, 0, 0, config.getPropertyPath());
3323
        return taxonList;
3324
    }
3325

    
3326
	@Override
3327
	@Transactional(readOnly = true)
3328
	public <S extends TaxonBase> Pager<IdentifiedEntityDTO<S>> findByIdentifier(
3329
			Class<S> clazz, String identifier, DefinedTerm identifierType, TaxonNode subtreeFilter,
3330
			MatchMode matchmode, boolean includeEntity, Integer pageSize,
3331
			Integer pageNumber,	List<String> propertyPaths) {
3332
		if (subtreeFilter == null){
3333
			return findByIdentifier(clazz, identifier, identifierType, matchmode, includeEntity, pageSize, pageNumber, propertyPaths);
3334
		}
3335

    
3336
		long numberOfResults = dao.countByIdentifier(clazz, identifier, identifierType, subtreeFilter, matchmode);
3337
        List<Object[]> daoResults = new ArrayList<>();
3338
        if(numberOfResults > 0) { // no point checking again
3339
        	daoResults = dao.findByIdentifier(clazz, identifier, identifierType, subtreeFilter,
3340
    				matchmode, includeEntity, pageSize, pageNumber, propertyPaths);
3341
        }
3342

    
3343
        List<IdentifiedEntityDTO<S>> result = new ArrayList<>();
3344
        for (Object[] daoObj : daoResults){
3345
        	if (includeEntity){
3346
        		result.add(new IdentifiedEntityDTO<>((DefinedTerm)daoObj[0], (String)daoObj[1], (S)daoObj[2]));
3347
        	}else{
3348
        		result.add(new IdentifiedEntityDTO<>((DefinedTerm)daoObj[0], (String)daoObj[1], (UUID)daoObj[2], (String)daoObj[3], null));
3349
        	}
3350
        }
3351
		return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, result);
3352
	}
3353

    
3354
	@Override
3355
    @Transactional(readOnly = true)
3356
    public <S extends TaxonBase> Pager<MarkedEntityDTO<S>> findByMarker(
3357
            Class<S> clazz, MarkerType markerType, Boolean markerValue,
3358
            TaxonNode subtreeFilter, boolean includeEntity, TaxonTitleType titleType,
3359
            Integer pageSize, Integer pageNumber, List<String> propertyPaths) {
3360
        if (subtreeFilter == null){
3361
            return super.findByMarker (clazz, markerType, markerValue, includeEntity, pageSize, pageNumber, propertyPaths);
3362
        }
3363

    
3364
        Long numberOfResults = dao.countByMarker(clazz, markerType, markerValue, subtreeFilter);
3365
        List<Object[]> daoResults = new ArrayList<>();
3366
        if(numberOfResults > 0) { // no point checking again
3367
            daoResults = dao.findByMarker(clazz, markerType, markerValue, subtreeFilter,
3368
                    includeEntity, titleType, pageSize, pageNumber, propertyPaths);
3369
        }
3370

    
3371
        List<MarkedEntityDTO<S>> result = new ArrayList<>();
3372
        for (Object[] daoObj : daoResults){
3373
            if (includeEntity){
3374
                result.add(new MarkedEntityDTO<S>((MarkerType)daoObj[0], (Boolean)daoObj[1], (S)daoObj[2]));
3375
            }else{
3376
                result.add(new MarkedEntityDTO<S>((MarkerType)daoObj[0], (Boolean)daoObj[1], (UUID)daoObj[2], (String)daoObj[3]));
3377
            }
3378
        }
3379
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, result);
3380
    }
3381

    
3382
    @Override
3383
	public UpdateResult moveFactualDateToAnotherTaxon(UUID fromTaxonUuid, UUID toTaxonUuid){
3384
		UpdateResult result = new UpdateResult();
3385

    
3386
		Taxon fromTaxon = (Taxon)dao.load(fromTaxonUuid);
3387
		Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
3388
    	for(TaxonDescription description : fromTaxon.getDescriptions()){
3389
              //reload to avoid session conflicts
3390
              description = HibernateProxyHelper.deproxy(description, TaxonDescription.class);
3391

    
3392
              String moveMessage = String.format("Description moved from %s", fromTaxon);
3393
              if(description.isProtectedTitleCache()){
3394
                  String separator = "";
3395
                  if(!StringUtils.isBlank(description.getTitleCache())){
3396
                      separator = " - ";
3397
                  }
3398
                  description.setTitleCache(description.getTitleCache() + separator + moveMessage, true);
3399
              }
3400
              Annotation annotation = Annotation.NewInstance(moveMessage, Language.getDefaultLanguage());
3401
              annotation.setAnnotationType(AnnotationType.TECHNICAL());
3402
              description.addAnnotation(annotation);
3403
              toTaxon.addDescription(description);
3404
              dao.saveOrUpdate(toTaxon);
3405
              dao.saveOrUpdate(fromTaxon);
3406
              result.addUpdatedObject(toTaxon);
3407
              result.addUpdatedObject(fromTaxon);
3408

    
3409
        }
3410

    
3411
    	return result;
3412
	}
3413

    
3414
	@Override
3415
	@Transactional(readOnly = false)
3416
	public UpdateResult swapSynonymAndAcceptedTaxon(UUID synonymUUid,
3417
			UUID acceptedTaxonUuid, boolean setNameInSource, boolean newUuidForAcceptedTaxon) {
3418
		TaxonBase<?> base = this.load(synonymUUid);
3419
		Synonym syn = HibernateProxyHelper.deproxy(base, Synonym.class);
3420
		base = this.load(acceptedTaxonUuid);
3421
		Taxon taxon = HibernateProxyHelper.deproxy(base, Taxon.class);
3422

    
3423
		return this.swapSynonymAndAcceptedTaxon(syn, taxon, setNameInSource, newUuidForAcceptedTaxon);
3424
	}
3425

    
3426
    @Override
3427
    public TaxonRelationshipsDTO listTaxonRelationships(UUID taxonUuid, Set<TaxonRelationshipType> directTypes,
3428
            Set<TaxonRelationshipType> inversTypes,
3429
            Direction direction, boolean groupMisapplications,
3430
            boolean includeUnpublished,
3431
            Integer pageSize, Integer pageNumber) {
3432
        TaxonBase<?> taxonBase = dao.load(taxonUuid);
3433
        if (taxonBase == null || !taxonBase.isInstanceOf(TaxonBase.class)){
3434
            //TODO handle
3435
            throw new RuntimeException("Taxon for uuid " + taxonUuid + " not found");
3436
        }else{
3437
            Taxon taxon = CdmBase.deproxy(taxonBase, Taxon.class);
3438
            boolean doDirect = (direction == null || direction == Direction.relatedTo);
3439
            boolean doInvers = (direction == null || direction == Direction.relatedFrom);
3440

    
3441
            TaxonRelationshipsDTO dto = new TaxonRelationshipsDTO();
3442

    
3443
            //TODO paging is difficult because misapplication string is an attribute
3444
            //of toplevel dto
3445
//        long numberOfResults = dao.countTaxonRelationships(taxon, type, includeUnpublished, TaxonRelationship.Direction.relatedFrom);
3446
//        List<TaxonRelationshipsDTO> results = new ArrayList<>();
3447
//        if(numberOfResults > 0) { // no point checking again
3448
//            results = dao.getTaxonRelationships(taxon, type, includeUnpublished, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedFrom);
3449
//        }
3450
//
3451
//        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);;
3452

    
3453
            //TODO languages
3454
            List<Language> languages = null;
3455
            if (doDirect){
3456
                direction = Direction.relatedTo;
3457
                //TODO order hints, property path
3458
                List<TaxonRelationship> relations = dao.getTaxonRelationships(taxon, directTypes, includeUnpublished, pageSize, pageNumber, null, null, direction.invers());
3459
                for (TaxonRelationship relation : relations){
3460
                    dto.addRelation(relation, direction, languages);
3461
                }
3462
            }
3463
            if (doInvers){
3464
                direction = Direction.relatedFrom;
3465
                //TODO order hints, property path
3466
                List<TaxonRelationship> relations = dao.getTaxonRelationships(taxon, inversTypes, includeUnpublished, pageSize, pageNumber, null, null, direction.invers());
3467
                for (TaxonRelationship relation : relations){
3468
                    dto.addRelation(relation, direction, languages);
3469
                }
3470
            }
3471
            if (groupMisapplications){
3472
                //TODO
3473
                dto.createMisapplicationString();
3474
            }
3475
            return dto;
3476
        }
3477
    }
3478
}
(89-89/97)