Project

General

Profile

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

    
134

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

    
145
    private static final Logger logger = Logger.getLogger(TaxonServiceImpl.class);
146

    
147
    public static final String POTENTIAL_COMBINATION_NAMESPACE = "Potential combination";
148

    
149
    public static final String INFERRED_EPITHET_NAMESPACE = "Inferred epithet";
150

    
151
    public static final String INFERRED_GENUS_NAMESPACE = "Inferred genus";
152

    
153
    @Autowired
154
    private ITaxonNodeDao taxonNodeDao;
155

    
156
    @Autowired
157
    private ITaxonNameDao nameDao;
158

    
159
    @Autowired
160
    private INameService nameService;
161

    
162
    @Autowired
163
    private IOccurrenceService occurrenceService;
164

    
165
    @Autowired
166
    private ITaxonNodeService nodeService;
167

    
168
    @Autowired
169
    private IDescriptionService descriptionService;
170
//
171
//    @Autowired
172
//    private IOrderedTermVocabularyDao orderedVocabularyDao;
173

    
174
    @Autowired
175
    private IOccurrenceDao occurrenceDao;
176

    
177
    @Autowired
178
    private IClassificationDao classificationDao;
179

    
180
    @Autowired
181
    private AbstractBeanInitializer beanInitializer;
182

    
183
    @Autowired
184
    private ILuceneIndexToolProvider luceneIndexToolProvider;
185

    
186
//************************ CONSTRUCTOR ****************************/
187
    public TaxonServiceImpl(){
188
        if (logger.isDebugEnabled()) { logger.debug("Load TaxonService Bean"); }
189
    }
190

    
191
// ****************************** METHODS ********************************/
192

    
193
    @Override
194
    public TaxonBase load(UUID uuid, boolean includeUnpublished, List<String> propertyPaths) {
195
        return dao.load(uuid, includeUnpublished, propertyPaths);
196
    }
197

    
198
    @Override
199
    public List<TaxonBase> searchByName(String name, boolean includeUnpublished, Reference sec) {
200
        return dao.getTaxaByName(name, includeUnpublished, sec);
201
    }
202

    
203
    @Override
204
    @Transactional(readOnly = false)
205
    public UpdateResult swapSynonymAndAcceptedTaxon(Synonym synonym, Taxon acceptedTaxon, boolean setNameInSource){
206
        UpdateResult result = new UpdateResult();
207
    	acceptedTaxon.removeSynonym(synonym);
208
    	TaxonName synonymName = synonym.getName();
209
    	boolean sameHomotypicGroup = synonymName.getHomotypicalGroup().equals(acceptedTaxon.getName().getHomotypicalGroup());
210

    
211
        synonymName.removeTaxonBase(synonym);
212

    
213
        TaxonName taxonName = HibernateProxyHelper.deproxy(acceptedTaxon.getName(), TaxonName.class);
214
        //taxonName.removeTaxonBase(acceptedTaxon);
215

    
216
        List<Synonym> synonyms = new ArrayList<>();
217
        for (Synonym syn: acceptedTaxon.getSynonyms()){
218
            syn = HibernateProxyHelper.deproxy(syn, Synonym.class);
219
            synonyms.add(syn);
220
        }
221
        for (Synonym syn: synonyms){
222
            acceptedTaxon.removeSynonym(syn);
223
        }
224
        Taxon newTaxon = acceptedTaxon.clone();
225
        newTaxon.setName(synonymName);
226
        newTaxon.setSec(synonym.getSec());
227
        for (Synonym syn: synonyms){
228
            if (!syn.getName().equals(newTaxon.getName())){
229
                newTaxon.addSynonym(syn, syn.getType());
230
            }
231
        }
232

    
233
        //move all data to new taxon
234
        //Move Taxon RelationShips to new Taxon
235
        for(TaxonRelationship taxonRelationship : newTaxon.getTaxonRelations()){
236
            newTaxon.removeTaxonRelation(taxonRelationship);
237
        }
238

    
239
        for(TaxonRelationship taxonRelationship : acceptedTaxon.getTaxonRelations()){
240
            Taxon fromTaxon = HibernateProxyHelper.deproxy(taxonRelationship.getFromTaxon());
241
            Taxon toTaxon = HibernateProxyHelper.deproxy(taxonRelationship.getToTaxon());
242
            if (fromTaxon == acceptedTaxon){
243
                newTaxon.addTaxonRelation(taxonRelationship.getToTaxon(), taxonRelationship.getType(),
244
                        taxonRelationship.getCitation(), taxonRelationship.getCitationMicroReference());
245

    
246
            }else if(toTaxon == acceptedTaxon){
247
               fromTaxon.addTaxonRelation(newTaxon, taxonRelationship.getType(),
248
                        taxonRelationship.getCitation(), taxonRelationship.getCitationMicroReference());
249
               saveOrUpdate(fromTaxon);
250

    
251
            }else{
252
                logger.warn("Taxon is not part of its own Taxonrelationship");
253
            }
254
            // Remove old relationships
255

    
256
            fromTaxon.removeTaxonRelation(taxonRelationship);
257
            toTaxon.removeTaxonRelation(taxonRelationship);
258
            taxonRelationship.setToTaxon(null);
259
            taxonRelationship.setFromTaxon(null);
260
        }
261

    
262
        //Move descriptions to new taxon
263
        List<TaxonDescription> descriptions = new ArrayList<TaxonDescription>( newTaxon.getDescriptions()); //to avoid concurrent modification errors (newAcceptedTaxon.addDescription() modifies also oldtaxon.descritpions())
264
        for(TaxonDescription description : descriptions){
265
            String message = "Description copied from former accepted taxon: %s (Old title: %s)";
266
            message = String.format(message, acceptedTaxon.getTitleCache(), description.getTitleCache());
267
            description.setTitleCache(message, true);
268
            if(setNameInSource){
269
                for (DescriptionElementBase element: description.getElements()){
270
                    for (DescriptionElementSource source: element.getSources()){
271
                        if (source.getNameUsedInSource() == null){
272
                            source.setNameUsedInSource(taxonName);
273
                        }
274
                    }
275
                }
276
            }
277
//            //oldTaxon.removeDescription(description, false);
278
 //           newTaxon.addDescription(description);
279
        }
280
        List<TaxonNode> nodes = new ArrayList<>(acceptedTaxon.getTaxonNodes());
281
        for (TaxonNode node: nodes){
282
            node = HibernateProxyHelper.deproxy(node, TaxonNode.class);
283
            TaxonNode parent = node.getParent();
284
            acceptedTaxon.removeTaxonNode(node);
285
            node.setTaxon(newTaxon);
286
            if (parent != null){
287
                parent.addChildNode(node, null, null);
288
            }
289

    
290
        }
291
        Synonym newSynonym = synonym.clone();
292
        newSynonym.setName(taxonName);
293
        newSynonym.setSec(acceptedTaxon.getSec());
294
        if (sameHomotypicGroup){
295
            newTaxon.addSynonym(newSynonym, SynonymType.HOMOTYPIC_SYNONYM_OF());
296
        }else{
297
            newTaxon.addSynonym(newSynonym, SynonymType.HETEROTYPIC_SYNONYM_OF());
298
        }
299

    
300
        saveOrUpdate(newSynonym);
301
        saveOrUpdate(newTaxon);
302
        TaxonDeletionConfigurator conf = new TaxonDeletionConfigurator();
303
        conf.setDeleteNameIfPossible(false);
304
        SynonymDeletionConfigurator confSyn = new SynonymDeletionConfigurator();
305
        confSyn.setDeleteNameIfPossible(false);
306
        result.setCdmEntity(newTaxon);
307

    
308
        DeleteResult deleteResult = deleteTaxon(acceptedTaxon.getUuid(), conf, null);
309
        if (synonym.isPersited()){
310
            deleteResult.includeResult(deleteSynonym(synonym, confSyn));
311
        }
312
        result.includeResult(deleteResult);
313
		return result;
314
    }
315

    
316
    @Override
317
    @Transactional(readOnly = false)
318
    public UpdateResult changeSynonymToAcceptedTaxon(Synonym synonym, Taxon acceptedTaxon, boolean deleteSynonym) {
319
        UpdateResult result = new UpdateResult();
320
        TaxonName acceptedName = acceptedTaxon.getName();
321
        TaxonName synonymName = synonym.getName();
322
        HomotypicalGroup synonymHomotypicGroup = synonymName.getHomotypicalGroup();
323

    
324
        //check synonym is not homotypic
325
        if (acceptedName.getHomotypicalGroup().equals(synonymHomotypicGroup)){
326
            String message = "The accepted taxon and the synonym are part of the same homotypical group and therefore can not be both accepted.";
327
            result.addException(new HomotypicalGroupChangeException(message));
328
            result.setAbort();
329
            return result;
330
        }
331

    
332
        Taxon newAcceptedTaxon = Taxon.NewInstance(synonymName, synonym.getSec(), synonym.getSecMicroReference());
333

    
334
        dao.save(newAcceptedTaxon);
335
        result.setCdmEntity(newAcceptedTaxon);
336
        SynonymType relTypeForGroup = SynonymType.HOMOTYPIC_SYNONYM_OF();
337
        List<Synonym> heteroSynonyms = acceptedTaxon.getSynonymsInGroup(synonymHomotypicGroup);
338

    
339
        for (Synonym heteroSynonym : heteroSynonyms){
340
            if (synonym.equals(heteroSynonym)){
341
                acceptedTaxon.removeSynonym(heteroSynonym, false);
342
            }else{
343
                //move synonyms in same homotypic group to new accepted taxon
344
                newAcceptedTaxon.addSynonym(heteroSynonym, relTypeForGroup);
345
            }
346
        }
347
        dao.saveOrUpdate(acceptedTaxon);
348
        result.addUpdatedObject(acceptedTaxon);
349
        if (deleteSynonym){
350

    
351
            try {
352
                this.dao.flush();
353
                SynonymDeletionConfigurator config = new SynonymDeletionConfigurator();
354
                config.setDeleteNameIfPossible(false);
355
                this.deleteSynonym(synonym, config);
356

    
357
            } catch (Exception e) {
358
                result.addException(e);
359
            }
360
        }
361

    
362
        return result;
363
    }
364

    
365
    @Override
366
    @Transactional(readOnly = false)
367
    public UpdateResult changeSynonymToAcceptedTaxon(UUID synonymUuid,
368
            UUID acceptedTaxonUuid,
369
            UUID newParentNodeUuid,
370
            boolean deleteSynonym)  {
371
        UpdateResult result = new UpdateResult();
372
        Synonym synonym = CdmBase.deproxy(dao.load(synonymUuid), Synonym.class);
373
        Taxon acceptedTaxon = CdmBase.deproxy(dao.load(acceptedTaxonUuid), Taxon.class);
374
        result =  changeSynonymToAcceptedTaxon(synonym, acceptedTaxon, deleteSynonym);
375
        Taxon newTaxon = (Taxon)result.getCdmEntity();
376
        TaxonNode newParentNode = taxonNodeDao.load(newParentNodeUuid);
377
        TaxonNode newNode = newParentNode.addChildTaxon(newTaxon, null, null);
378
        taxonNodeDao.save(newNode);
379
        result.addUpdatedObject(newTaxon);
380
        result.addUpdatedObject(acceptedTaxon);
381
        result.setCdmEntity(newNode);
382
        return result;
383
    }
384

    
385
    @Override
386
    @Transactional(readOnly = false)
387
    public UpdateResult changeSynonymToRelatedTaxon(UUID synonymUuid,
388
            UUID toTaxonUuid,
389
            TaxonRelationshipType taxonRelationshipType,
390
            Reference citation,
391
            String microcitation){
392

    
393
        UpdateResult result = new UpdateResult();
394
        Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
395
        Synonym synonym = (Synonym) dao.load(synonymUuid);
396
        result = changeSynonymToRelatedTaxon(synonym, toTaxon, taxonRelationshipType, citation, microcitation);
397
        Taxon relatedTaxon = (Taxon)result.getCdmEntity();
398
//        result.setCdmEntity(relatedTaxon);
399
        result.addUpdatedObject(relatedTaxon);
400
        result.addUpdatedObject(toTaxon);
401
        return result;
402
    }
403

    
404
    @Override
405
    @Transactional(readOnly = false)
406
    public UpdateResult changeSynonymToRelatedTaxon(Synonym synonym, Taxon toTaxon, TaxonRelationshipType taxonRelationshipType, Reference citation, String microcitation){
407
        // Get name from synonym
408
        if (synonym == null){
409
            return null;
410
        }
411

    
412
        UpdateResult result = new UpdateResult();
413

    
414
        TaxonName synonymName = synonym.getName();
415

    
416
      /*  // remove synonym from taxon
417
        toTaxon.removeSynonym(synonym);
418
*/
419
        // Create a taxon with synonym name
420
        Taxon fromTaxon = Taxon.NewInstance(synonymName, null);
421
        save(fromTaxon);
422
        fromTaxon.setAppendedPhrase(synonym.getAppendedPhrase());
423

    
424
        // Add taxon relation
425
        fromTaxon.addTaxonRelation(toTaxon, taxonRelationshipType, citation, microcitation);
426
        result.setCdmEntity(fromTaxon);
427
        // since we are swapping names, we have to detach the name from the synonym completely.
428
        // Otherwise the synonym will still be in the list of typified names.
429
       // synonym.getName().removeTaxonBase(synonym);
430
        result.includeResult(this.deleteSynonym(synonym, null));
431

    
432
        return result;
433
    }
434

    
435
    @Transactional(readOnly = false)
436
    @Override
437
    public void changeHomotypicalGroupOfSynonym(Synonym synonym, HomotypicalGroup newHomotypicalGroup,
438
            Taxon targetTaxon, boolean setBasionymRelationIfApplicable){
439
        // Get synonym name
440
        TaxonName synonymName = synonym.getName();
441
        HomotypicalGroup oldHomotypicalGroup = synonymName.getHomotypicalGroup();
442

    
443
        // Switch groups
444
        oldHomotypicalGroup.removeTypifiedName(synonymName, false);
445
        newHomotypicalGroup.addTypifiedName(synonymName);
446

    
447
        //remove existing basionym relationships
448
        synonymName.removeBasionyms();
449

    
450
        //add basionym relationship
451
        if (setBasionymRelationIfApplicable){
452
            Set<TaxonName> basionyms = newHomotypicalGroup.getBasionyms();
453
            for (TaxonName basionym : basionyms){
454
                synonymName.addBasionym(basionym);
455
            }
456
        }
457

    
458
        //set synonym relationship correctly
459
        Taxon acceptedTaxon = synonym.getAcceptedTaxon();
460

    
461
        boolean hasNewTargetTaxon = targetTaxon != null && !targetTaxon.equals(acceptedTaxon);
462
        if (acceptedTaxon != null){
463

    
464
            HomotypicalGroup acceptedGroup = acceptedTaxon.getHomotypicGroup();
465
            boolean isHomotypicToTaxon = acceptedGroup.equals(newHomotypicalGroup);
466
            SynonymType newRelationType = isHomotypicToTaxon? SynonymType.HOMOTYPIC_SYNONYM_OF() : SynonymType.HETEROTYPIC_SYNONYM_OF();
467
            synonym.setType(newRelationType);
468

    
469
            if (hasNewTargetTaxon){
470
                acceptedTaxon.removeSynonym(synonym, false);
471
            }
472
        }
473
        if (hasNewTargetTaxon ){
474
            @SuppressWarnings("null")
475
            HomotypicalGroup acceptedGroup = targetTaxon.getHomotypicGroup();
476
            boolean isHomotypicToTaxon = acceptedGroup.equals(newHomotypicalGroup);
477
            SynonymType relType = isHomotypicToTaxon? SynonymType.HOMOTYPIC_SYNONYM_OF() : SynonymType.HETEROTYPIC_SYNONYM_OF();
478
            targetTaxon.addSynonym(synonym, relType);
479
        }
480
    }
481

    
482
    @Override
483
    @Transactional(readOnly = false)
484
    public UpdateResult updateCaches(Class<? extends TaxonBase> clazz, Integer stepSize, IIdentifiableEntityCacheStrategy<TaxonBase> cacheStrategy, IProgressMonitor monitor) {
485
        if (clazz == null){
486
            clazz = TaxonBase.class;
487
        }
488
        return super.updateCachesImpl(clazz, stepSize, cacheStrategy, monitor);
489
    }
490

    
491
    @Override
492
    @Autowired
493
    protected void setDao(ITaxonDao dao) {
494
        this.dao = dao;
495
    }
496

    
497
    @Override
498
    public <T extends TaxonBase> Pager<T> findTaxaByName(Class<T> clazz, String uninomial,	String infragenericEpithet, String specificEpithet,
499
            String infraspecificEpithet, String authorshipCache, Rank rank, Integer pageSize,Integer pageNumber, List<String> propertyPaths) {
500
        long numberOfResults = dao.countTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, authorshipCache, rank);
501

    
502
        List<T> results = new ArrayList<>();
503
        if(numberOfResults > 0) { // no point checking again
504
            results = dao.findTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, authorshipCache, rank,
505
                    pageSize, pageNumber, propertyPaths);
506
        }
507

    
508
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
509
    }
510

    
511
    @Override
512
    public <T extends TaxonBase> List<T> listTaxaByName(Class<T> clazz, String uninomial, String infragenericEpithet, String specificEpithet,
513
            String infraspecificEpithet, String authorshipCache, Rank rank, Integer pageSize,Integer pageNumber, List<String> propertyPaths) {
514

    
515
        return findTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infragenericEpithet, authorshipCache, rank,
516
                pageSize, pageNumber, propertyPaths).getRecords();
517
    }
518

    
519
    @Override
520
    public List<TaxonRelationship> listToTaxonRelationships(Taxon taxon, TaxonRelationshipType type,
521
            boolean includeUnpublished, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths){
522
        long numberOfResults = dao.countTaxonRelationships(taxon, type, includeUnpublished, TaxonRelationship.Direction.relatedTo);
523

    
524
        List<TaxonRelationship> results = new ArrayList<>();
525
        if(numberOfResults > 0) { // no point checking again
526
            results = dao.getTaxonRelationships(taxon, type, includeUnpublished, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedTo);
527
        }
528
        return results;
529
    }
530

    
531
    @Override
532
    public Pager<TaxonRelationship> pageToTaxonRelationships(Taxon taxon, TaxonRelationshipType type,
533
            boolean includeUnpublished, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
534
        long numberOfResults = dao.countTaxonRelationships(taxon, type, includeUnpublished, TaxonRelationship.Direction.relatedTo);
535

    
536
        List<TaxonRelationship> results = new ArrayList<>();
537
        if(numberOfResults > 0) { // no point checking again
538
            results = dao.getTaxonRelationships(taxon, type, includeUnpublished, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedTo);
539
        }
540
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
541
    }
542

    
543
    @Override
544
    public List<TaxonRelationship> listFromTaxonRelationships(Taxon taxon, TaxonRelationshipType type,
545
            boolean includeUnpublished, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths){
546
        long numberOfResults = dao.countTaxonRelationships(taxon, type, includeUnpublished, TaxonRelationship.Direction.relatedFrom);
547

    
548
        List<TaxonRelationship> results = new ArrayList<>();
549
        if(numberOfResults > 0) { // no point checking again
550
            results = dao.getTaxonRelationships(taxon, type, includeUnpublished, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedFrom);
551
        }
552
        return results;
553
    }
554

    
555
    @Override
556
    public Pager<TaxonRelationship> pageFromTaxonRelationships(Taxon taxon, TaxonRelationshipType type,
557
            boolean includeUnpublished, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
558
        long numberOfResults = dao.countTaxonRelationships(taxon, type, includeUnpublished, TaxonRelationship.Direction.relatedFrom);
559

    
560
        List<TaxonRelationship> results = new ArrayList<>();
561
        if(numberOfResults > 0) { // no point checking again
562
            results = dao.getTaxonRelationships(taxon, type, includeUnpublished, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedFrom);
563
        }
564
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
565
    }
566

    
567
    @Override
568
    public List<TaxonRelationship> listTaxonRelationships(Set<TaxonRelationshipType> types,
569
            Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
570

    
571
        Long numberOfResults = dao.countTaxonRelationships(types);
572
        List<TaxonRelationship> results = new ArrayList<>();
573
        if(numberOfResults > 0) {
574
            results = dao.getTaxonRelationships(types, pageSize, pageNumber, orderHints, propertyPaths);
575
        }
576
        return results;
577
    }
578

    
579
    @Override
580
    public <S extends TaxonBase> Pager<S> page(Class<S> clazz, List<Restriction<?>> restrictions, Integer pageSize,
581
            Integer pageIndex, List<OrderHint> orderHints, List<String> propertyPaths) {
582
        return page(clazz, restrictions, pageSize, pageIndex, orderHints, propertyPaths, INCLUDE_UNPUBLISHED);
583
    }
584

    
585
    @Override
586
    public <S extends TaxonBase> Pager<S> page(Class<S> clazz, List<Restriction<?>> restrictions, Integer pageSize,
587
            Integer pageIndex, List<OrderHint> orderHints, List<String> propertyPaths, boolean includeUnpublished) {
588

    
589
        List<S> records;
590
        long resultSize = dao.count(clazz, restrictions);
591
        if(AbstractPagerImpl.hasResultsInRange(resultSize, pageIndex, pageSize)){
592
            records = dao.list(clazz, restrictions, pageSize, pageIndex, orderHints, propertyPaths, includeUnpublished);
593
        } else {
594
            records = new ArrayList<>();
595
        }
596
        Pager<S> pager = new DefaultPagerImpl<>(pageIndex, resultSize, pageSize, records);
597
        return pager;
598
    }
599

    
600
    @Override
601
    public Taxon findAcceptedTaxonFor(UUID synonymUuid, UUID classificationUuid,
602
            boolean includeUnpublished, List<String> propertyPaths) throws UnpublishedException{
603

    
604
        Synonym synonym = null;
605

    
606
        try {
607
            synonym = (Synonym) dao.load(synonymUuid);
608
            checkPublished(synonym, includeUnpublished, "Synoym is unpublished.");
609
        } catch (ClassCastException e){
610
            throw new EntityNotFoundException("The TaxonBase entity referenced by " + synonymUuid + " is not a Synonmy");
611
        } catch (NullPointerException e){
612
            throw new EntityNotFoundException("No TaxonBase entity found for " + synonymUuid);
613
        }
614

    
615
        Classification classificationFilter = null;
616
        if(classificationUuid != null){
617
            try {
618
                classificationFilter = classificationDao.load(classificationUuid);
619
            } catch (NullPointerException e){
620
                //TODO not sure, why an NPE should be thrown in the above load method
621
                throw new EntityNotFoundException("No Classification entity found for " + classificationUuid);
622
            }
623
            if(classificationFilter == null){
624
                throw new EntityNotFoundException("No Classification entity found for " + classificationUuid);
625
            }
626
        }
627

    
628
        long count = dao.countAcceptedTaxonFor(synonym, classificationFilter) ;
629
        if(count > 0){
630
            Taxon result = dao.acceptedTaxonFor(synonym, classificationFilter, propertyPaths);
631
            checkPublished(result, includeUnpublished, "Accepted taxon unpublished");
632
            return result;
633
        }else{
634
            return null;
635
        }
636
    }
637

    
638
    @Override
639
    public Set<Taxon> listRelatedTaxa(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, Integer maxDepth,
640
            boolean includeUnpublished, Integer limit, Integer start, List<String> propertyPaths) {
641

    
642
        Set<Taxon> relatedTaxa = collectRelatedTaxa(taxon, includeRelationships, new HashSet<>(), includeUnpublished, maxDepth);
643
        relatedTaxa.remove(taxon);
644
        beanInitializer.initializeAll(relatedTaxa, propertyPaths);
645
        return relatedTaxa;
646
    }
647

    
648
    /**
649
     * Recursively collect related taxa for the given <code>taxon</code> . The returned list will also include the
650
     *  <code>taxon</code> supplied as parameter.
651
     *
652
     * @param taxon
653
     * @param includeRelationships
654
     * @param taxa
655
     * @param maxDepth can be <code>null</code> for infinite depth
656
     * @return
657
     */
658
    private Set<Taxon> collectRelatedTaxa(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, Set<Taxon> taxa,
659
            boolean includeUnpublished, Integer maxDepth) {
660

    
661
        if(taxa.isEmpty()) {
662
            taxa.add(taxon);
663
        }
664

    
665
        if(includeRelationships.isEmpty()){
666
            return taxa;
667
        }
668

    
669
        if(maxDepth != null) {
670
            maxDepth--;
671
        }
672
        if(logger.isDebugEnabled()){
673
            logger.debug("collecting related taxa for " + taxon + " with maxDepth=" + maxDepth);
674
        }
675
        List<TaxonRelationship> taxonRelationships = dao.getTaxonRelationships(taxon,
676
                (Set<TaxonRelationshipType>)null, includeUnpublished, null, null, null, null, null);
677
        for (TaxonRelationship taxRel : taxonRelationships) {
678

    
679
            // skip invalid data
680
            if (taxRel.getToTaxon() == null || taxRel.getFromTaxon() == null || taxRel.getType() == null) {
681
                continue;
682
            }
683
            // filter by includeRelationships
684
            for (TaxonRelationshipEdge relationshipEdgeFilter : includeRelationships) {
685
                if ( relationshipEdgeFilter.getRelationshipTypes().equals(taxRel.getType()) ) {
686
                    if (relationshipEdgeFilter.getDirections().contains(Direction.relatedTo) && !taxa.contains(taxRel.getToTaxon())) {
687
                        if(logger.isDebugEnabled()){
688
                            logger.debug(maxDepth + ": " + taxon.getTitleCache() + " --[" + taxRel.getType().getLabel() + "]--> " + taxRel.getToTaxon().getTitleCache());
689
                        }
690
                        taxa.add(taxRel.getToTaxon());
691
                        if(maxDepth == null || maxDepth > 0) {
692
                            taxa.addAll(collectRelatedTaxa(taxRel.getToTaxon(), includeRelationships, taxa, includeUnpublished, maxDepth));
693
                        }
694
                    }
695
                    if(relationshipEdgeFilter.getDirections().contains(Direction.relatedFrom) && !taxa.contains(taxRel.getFromTaxon())) {
696
                        taxa.add(taxRel.getFromTaxon());
697
                        if(logger.isDebugEnabled()){
698
                            logger.debug(maxDepth + ": " +taxRel.getFromTaxon().getTitleCache() + " --[" + taxRel.getType().getLabel() + "]--> " + taxon.getTitleCache() );
699
                        }
700
                        if(maxDepth == null || maxDepth > 0) {
701
                            taxa.addAll(collectRelatedTaxa(taxRel.getFromTaxon(), includeRelationships, taxa, includeUnpublished, maxDepth));
702
                        }
703
                    }
704
                }
705
            }
706
        }
707
        return taxa;
708
    }
709

    
710
    @Override
711
    public Pager<Synonym> getSynonyms(Taxon taxon,	SynonymType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
712
        Long numberOfResults = dao.countSynonyms(taxon, type);
713

    
714
        List<Synonym> results = new ArrayList<>();
715
        if(numberOfResults > 0) { // no point checking again
716
            results = dao.getSynonyms(taxon, type, pageSize, pageNumber, orderHints, propertyPaths);
717
        }
718

    
719
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
720
    }
721

    
722
    @Override
723
    public List<List<Synonym>> getSynonymsByHomotypicGroup(Taxon taxon, List<String> propertyPaths){
724
        List<List<Synonym>> result = new ArrayList<>();
725
        taxon = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
726
        HomotypicGroupTaxonComparator comparator = new HomotypicGroupTaxonComparator(taxon);
727

    
728
        //homotypic
729
        result.add(taxon.getHomotypicSynonymsByHomotypicGroup(comparator));
730

    
731
        //heterotypic
732
        List<HomotypicalGroup> homotypicalGroups = taxon.getHeterotypicSynonymyGroups();  //currently the list is sorted by the Taxon.defaultTaxonComparator
733
        for(HomotypicalGroup homotypicalGroup : homotypicalGroups){
734
            result.add(taxon.getSynonymsInGroup(homotypicalGroup, comparator));
735
        }
736

    
737
        return result;
738
    }
739

    
740
    @Override
741
    public List<Synonym> getHomotypicSynonymsByHomotypicGroup(Taxon taxon, List<String> propertyPaths){
742
        Taxon t = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
743
        HomotypicGroupTaxonComparator comparator = new HomotypicGroupTaxonComparator(taxon);
744

    
745
        return t.getHomotypicSynonymsByHomotypicGroup(comparator);
746
    }
747

    
748
    @Override
749
    public List<List<Synonym>> getHeterotypicSynonymyGroups(Taxon taxon, List<String> propertyPaths){
750
        Taxon t = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
751
        List<HomotypicalGroup> homotypicalGroups = t.getHeterotypicSynonymyGroups();
752
        List<List<Synonym>> heterotypicSynonymyGroups = new ArrayList<>(homotypicalGroups.size());
753
        for(HomotypicalGroup homotypicalGroup : homotypicalGroups){
754
            heterotypicSynonymyGroups.add(t.getSynonymsInGroup(homotypicalGroup));
755
        }
756
        return heterotypicSynonymyGroups;
757
    }
758

    
759
    @Override
760
    public List<UuidAndTitleCache<? extends IdentifiableEntity>> findTaxaAndNamesForEditor(IFindTaxaAndNamesConfigurator config){
761

    
762
        if (config.isDoSynonyms() || config.isDoTaxa() || config.isDoNamesWithoutTaxa() || config.isDoTaxaByCommonNames()){
763
        	return dao.getTaxaByNameForEditor(config.isDoTaxa(), config.isDoSynonyms(), config.isDoNamesWithoutTaxa(),
764
        	        config.isDoMisappliedNames(), config.isDoTaxaByCommonNames(), config.isIncludeUnpublished(),
765
        	        config.getTitleSearchStringSqlized(), config.getClassification(), config.getSubtree(),
766
        	        config.getMatchMode(), config.getNamedAreas(), config.getOrder());
767
        }else{
768
            return new ArrayList<>();
769
        }
770
    }
771

    
772
    @Override
773
    public Pager<IdentifiableEntity> findTaxaAndNames(IFindTaxaAndNamesConfigurator configurator) {
774

    
775
        @SuppressWarnings("rawtypes")
776
        List<IdentifiableEntity> results = new ArrayList<>();
777
        long numberOfResults = 0; // overall number of results (as opposed to number of results per page)
778
        List<TaxonBase> taxa = null;
779

    
780
        // Taxa and synonyms
781
        long numberTaxaResults = 0L;
782

    
783
        List<String> propertyPath = new ArrayList<>();
784
        if(configurator.getTaxonPropertyPath() != null){
785
            propertyPath.addAll(configurator.getTaxonPropertyPath());
786
        }
787

    
788
        if (configurator.isDoMisappliedNames() || configurator.isDoSynonyms() || configurator.isDoTaxa() || configurator.isDoTaxaByCommonNames()){
789
            if(configurator.getPageSize() != null){ // no point counting if we need all anyway
790
                numberTaxaResults =
791
                    dao.countTaxaByName(configurator.isDoTaxa(),configurator.isDoSynonyms(), configurator.isDoMisappliedNames(),
792
                        configurator.isDoTaxaByCommonNames(), configurator.isDoIncludeAuthors(), configurator.getTitleSearchStringSqlized(),
793
                        configurator.getClassification(), configurator.getSubtree(), configurator.getMatchMode(),
794
                        configurator.getNamedAreas(), configurator.isIncludeUnpublished());
795
            }
796

    
797
            if(configurator.getPageSize() == null || numberTaxaResults > configurator.getPageSize() * configurator.getPageNumber()){ // no point checking again if less results
798
                taxa = dao.getTaxaByName(configurator.isDoTaxa(), configurator.isDoSynonyms(),
799
                    configurator.isDoMisappliedNames(), configurator.isDoTaxaByCommonNames(), configurator.isDoIncludeAuthors(),
800
                    configurator.getTitleSearchStringSqlized(), configurator.getClassification(), configurator.getSubtree(),
801
                    configurator.getMatchMode(), configurator.getNamedAreas(), configurator.isIncludeUnpublished(),
802
                    configurator.getOrder(), configurator.getPageSize(), configurator.getPageNumber(), propertyPath);
803
            }
804
        }
805

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

    
808
        if(taxa != null){
809
            results.addAll(taxa);
810
        }
811

    
812
        numberOfResults += numberTaxaResults;
813

    
814
        // Names without taxa
815
        if (configurator.isDoNamesWithoutTaxa()) {
816
            int numberNameResults = 0;
817

    
818
            List<TaxonName> names =
819
                nameDao.findByName(configurator.isDoIncludeAuthors(), configurator.getTitleSearchStringSqlized(), configurator.getMatchMode(),
820
                        configurator.getPageSize(), configurator.getPageNumber(), null, configurator.getTaxonNamePropertyPath());
821
            if (logger.isDebugEnabled()) { logger.debug(names.size() + " matching name(s) found"); }
822
            if (names.size() > 0) {
823
                for (TaxonName taxonName : names) {
824
                    if (taxonName.getTaxonBases().size() == 0) {
825
                        results.add(taxonName);
826
                        numberNameResults++;
827
                    }
828
                }
829
                if (logger.isDebugEnabled()) { logger.debug(numberNameResults + " matching name(s) without taxa found"); }
830
                numberOfResults += numberNameResults;
831
            }
832
        }
833

    
834
       return new DefaultPagerImpl<> (configurator.getPageNumber(), numberOfResults, configurator.getPageSize(), results);
835
    }
836

    
837
    public List<UuidAndTitleCache<TaxonBase>> getTaxonUuidAndTitleCache(Integer limit, String pattern){
838
        return dao.getUuidAndTitleCache(limit, pattern);
839
    }
840

    
841
    @Override
842
    public List<Media> listTaxonDescriptionMedia(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, boolean limitToGalleries, List<String> propertyPath){
843
        return listMedia(taxon, includeRelationships, limitToGalleries, true, false, false, propertyPath);
844
    }
845

    
846
    @Override
847
    public List<Media> listMedia(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships,
848
            Boolean limitToGalleries, Boolean includeTaxonDescriptions, Boolean includeOccurrences,
849
            Boolean includeTaxonNameDescriptions, List<String> propertyPath) {
850

    
851
        //TODO let inherit
852
        boolean includeUnpublished = INCLUDE_UNPUBLISHED;
853

    
854
    //    logger.setLevel(Level.TRACE);
855
//        Logger.getLogger("org.hibernate.SQL").setLevel(Level.TRACE);
856

    
857
        logger.trace("listMedia() - START");
858

    
859
        Set<Taxon> taxa = new HashSet<>();
860
        List<Media> taxonMedia = new ArrayList<>();
861
        List<Media> nonImageGalleryImages = new ArrayList<>();
862

    
863
        if (limitToGalleries == null) {
864
            limitToGalleries = false;
865
        }
866

    
867
        // --- resolve related taxa
868
        if (includeRelationships != null && ! includeRelationships.isEmpty()) {
869
            logger.trace("listMedia() - resolve related taxa");
870
            taxa = listRelatedTaxa(taxon, includeRelationships, null, includeUnpublished, null, null, null);
871
        }
872

    
873
        taxa.add((Taxon) dao.load(taxon.getUuid()));
874

    
875
        if(includeTaxonDescriptions != null && includeTaxonDescriptions){
876
            logger.trace("listMedia() - includeTaxonDescriptions");
877
            List<TaxonDescription> taxonDescriptions = new ArrayList<>();
878
            // --- TaxonDescriptions
879
            for (Taxon t : taxa) {
880
                taxonDescriptions.addAll(descriptionService.listTaxonDescriptions(t, null, null, null, null, propertyPath));
881
            }
882
            for (TaxonDescription taxonDescription : taxonDescriptions) {
883
                if (!limitToGalleries || taxonDescription.isImageGallery()) {
884
                    for (DescriptionElementBase element : taxonDescription.getElements()) {
885
                        for (Media media : element.getMedia()) {
886
                            if(taxonDescription.isImageGallery()){
887
                                taxonMedia.add(media);
888
                            }
889
                            else{
890
                                nonImageGalleryImages.add(media);
891
                            }
892
                        }
893
                    }
894
                }
895
            }
896
            //put images from image gallery first (#3242)
897
            taxonMedia.addAll(nonImageGalleryImages);
898
        }
899

    
900

    
901
        if(includeOccurrences != null && includeOccurrences) {
902
            logger.trace("listMedia() - includeOccurrences");
903
            @SuppressWarnings("rawtypes")
904
            Set<SpecimenOrObservationBase> specimensOrObservations = new HashSet<>();
905
            // --- Specimens
906
            for (Taxon t : taxa) {
907
                specimensOrObservations.addAll(occurrenceDao.listByAssociatedTaxon(null, t, null, null, null, null));
908
            }
909
            for (SpecimenOrObservationBase<?> occurrence : specimensOrObservations) {
910

    
911
//            	direct media removed from specimen #3597
912
//              taxonMedia.addAll(occurrence.getMedia());
913

    
914
                // SpecimenDescriptions
915
                Set<SpecimenDescription> specimenDescriptions = occurrence.getSpecimenDescriptions();
916
                for (DescriptionBase<?> specimenDescription : specimenDescriptions) {
917
                    if (!limitToGalleries || specimenDescription.isImageGallery()) {
918
                        Set<DescriptionElementBase> elements = specimenDescription.getElements();
919
                        for (DescriptionElementBase element : elements) {
920
                            for (Media media : element.getMedia()) {
921
                                taxonMedia.add(media);
922
                            }
923
                        }
924
                    }
925
                }
926

    
927
                if (occurrence.isInstanceOf(DerivedUnit.class)) {
928
                    DerivedUnit derivedUnit = CdmBase.deproxy(occurrence, DerivedUnit.class);
929
                    // Collection
930
                    //TODO why may collections have media attached? #
931
                    if (derivedUnit.getCollection() != null){
932
                        taxonMedia.addAll(derivedUnit.getCollection().getMedia());
933
                    }
934
                }
935
                //media in hierarchy
936
                taxonMedia.addAll(occurrenceService.getMediainHierarchy(occurrence, null, null, propertyPath).getRecords());
937
            }
938
        }
939

    
940
        if(includeTaxonNameDescriptions != null && includeTaxonNameDescriptions) {
941
            logger.trace("listMedia() - includeTaxonNameDescriptions");
942
            // --- TaxonNameDescription
943
            Set<TaxonNameDescription> nameDescriptions = new HashSet<>();
944
            for (Taxon t : taxa) {
945
                nameDescriptions .addAll(t.getName().getDescriptions());
946
            }
947
            for(TaxonNameDescription nameDescription: nameDescriptions){
948
                if (!limitToGalleries || nameDescription.isImageGallery()) {
949
                    Set<DescriptionElementBase> elements = nameDescription.getElements();
950
                    for (DescriptionElementBase element : elements) {
951
                        for (Media media : element.getMedia()) {
952
                            taxonMedia.add(media);
953
                        }
954
                    }
955
                }
956
            }
957
        }
958

    
959
        logger.trace("listMedia() - initialize");
960
        beanInitializer.initializeAll(taxonMedia, propertyPath);
961

    
962
        logger.trace("listMedia() - END");
963

    
964
        return taxonMedia;
965
    }
966

    
967
    @Override
968
    public List<TaxonBase> findTaxaByID(Set<Integer> listOfIDs) {
969
        return this.dao.loadList(listOfIDs, null, null);
970
    }
971

    
972
    @Override
973
    public TaxonBase findTaxonByUuid(UUID uuid, List<String> propertyPaths){
974
        return this.dao.findByUuid(uuid, null ,propertyPaths);
975
    }
976

    
977
    @Override
978
    public long countSynonyms(boolean onlyAttachedToTaxon){
979
        return this.dao.countSynonyms(onlyAttachedToTaxon);
980
    }
981

    
982
    @Override
983
    @Transactional(readOnly=false)
984
    public DeleteResult deleteTaxon(UUID taxonUUID, TaxonDeletionConfigurator config, UUID classificationUuid)  {
985

    
986
    	if (config == null){
987
            config = new TaxonDeletionConfigurator();
988
        }
989
    	Taxon taxon = (Taxon)dao.load(taxonUUID);
990
    	DeleteResult result = new DeleteResult();
991
    	if (taxon == null){
992
    	    result.setAbort();
993
    	    result.addException(new Exception ("The taxon was already deleted."));
994
    	    return result;
995
    	}
996
    	taxon = HibernateProxyHelper.deproxy(taxon);
997
    	Classification classification = HibernateProxyHelper.deproxy(classificationDao.load(classificationUuid), Classification.class);
998
    	config.setClassificationUuid(classificationUuid);
999
        result = isDeletable(taxonUUID, config);
1000

    
1001
        if (result.isOk()){
1002
            // --- DeleteSynonymRelations
1003
            if (config.isDeleteSynonymRelations()){
1004
                boolean removeSynonymNameFromHomotypicalGroup = false;
1005
                // use tmp Set to avoid concurrent modification
1006
                Set<Synonym> synsToDelete = new HashSet<>();
1007
                synsToDelete.addAll(taxon.getSynonyms());
1008
                for (Synonym synonym : synsToDelete){
1009
                    taxon.removeSynonym(synonym, removeSynonymNameFromHomotypicalGroup);
1010

    
1011
                    // --- DeleteSynonymsIfPossible
1012
                    if (config.isDeleteSynonymsIfPossible()){
1013
                        //TODO which value
1014
                        boolean newHomotypicGroupIfNeeded = true;
1015
                        SynonymDeletionConfigurator synConfig = new SynonymDeletionConfigurator();
1016
                        result.includeResult(deleteSynonym(synonym, synConfig));
1017
                    }
1018
                }
1019
            }
1020

    
1021
            // --- DeleteTaxonRelationships
1022
            if (! config.isDeleteTaxonRelationships()){
1023
                if (taxon.getTaxonRelations().size() > 0){
1024
                    result.setAbort();
1025
                    result.addException(new Exception("Taxon can't be deleted as it is related to another taxon. " +
1026
                            "Remove taxon from all relations to other taxa prior to deletion."));
1027
                }
1028
            } else{
1029
                TaxonDeletionConfigurator configRelTaxon = new TaxonDeletionConfigurator();
1030
                configRelTaxon.setDeleteTaxonNodes(false);
1031
                configRelTaxon.setDeleteConceptRelationships(true);
1032

    
1033
                for (TaxonRelationship taxRel: taxon.getTaxonRelations()){
1034
                    if (config.isDeleteMisappliedNames()
1035
                            && taxRel.getType().isMisappliedName()
1036
                            && taxon.equals(taxRel.getToTaxon())){
1037
                        this.deleteTaxon(taxRel.getFromTaxon().getUuid(), config, classificationUuid);
1038
                    } else if (config.isDeleteConceptRelationships() && taxRel.getType().isConceptRelationship()){
1039

    
1040
                        if (taxon.equals(taxRel.getToTaxon()) && isDeletable(taxRel.getFromTaxon().getUuid(), configRelTaxon).isOk()){
1041
                            this.deleteTaxon(taxRel.getFromTaxon().getUuid(), configRelTaxon, classificationUuid);
1042
                        }else if (isDeletable(taxRel.getToTaxon().getUuid(), configRelTaxon).isOk()){
1043
                            this.deleteTaxon(taxRel.getToTaxon().getUuid(), configRelTaxon, classificationUuid);
1044
                        }
1045
                    }
1046
                    taxon.removeTaxonRelation(taxRel);
1047
                }
1048
            }
1049

    
1050
            //    	TaxonDescription
1051
            if (config.isDeleteDescriptions()){
1052
                Set<TaxonDescription> descriptions = taxon.getDescriptions();
1053
                List<TaxonDescription> removeDescriptions = new ArrayList<>();
1054
                for (TaxonDescription desc: descriptions){
1055
                    //TODO use description delete configurator ?
1056
                    //FIXME check if description is ALWAYS deletable
1057
                    if (desc.getDescribedSpecimenOrObservation() != null){
1058
                        result.setAbort();
1059
                        result.addException(new Exception("Taxon can't be deleted as it is used in a TaxonDescription" +
1060
                                " which also describes specimens or observations"));
1061
                        break;
1062
                    }
1063
                    removeDescriptions.add(desc);
1064
                }
1065
                if (result.isOk()){
1066
                    for (TaxonDescription desc: removeDescriptions){
1067
                        taxon.removeDescription(desc);
1068
                        descriptionService.delete(desc);
1069
                    }
1070
                } else {
1071
                    return result;
1072
                }
1073
            }
1074

    
1075
             if (! config.isDeleteTaxonNodes() || (!config.isDeleteInAllClassifications() && classification == null && taxon.getTaxonNodes().size() > 1)){
1076
                 result.addException(new Exception( "Taxon can't be deleted as it is used in more than one classification."));
1077
             }else{
1078
                 if (taxon.getTaxonNodes().size() != 0){
1079
                    Set<TaxonNode> nodes = taxon.getTaxonNodes();
1080
                    Iterator<TaxonNode> iterator = nodes.iterator();
1081
                    TaxonNode node = null;
1082
                    boolean deleteChildren;
1083
                    if (config.getTaxonNodeConfig().getChildHandling().equals(ChildHandling.DELETE)){
1084
                        deleteChildren = true;
1085
                    }else {
1086
                        deleteChildren = false;
1087
                    }
1088
                    boolean success = true;
1089
                    if (!config.isDeleteInAllClassifications() && !(classification == null)){
1090
                        while (iterator.hasNext()){
1091
                            node = iterator.next();
1092
                            if (node.getClassification().equals(classification)){
1093
                                break;
1094
                            }
1095
                            node = null;
1096
                        }
1097
                        if (node != null){
1098
                            HibernateProxyHelper.deproxy(node, TaxonNode.class);
1099
                            success =taxon.removeTaxonNode(node, deleteChildren);
1100
                            nodeService.delete(node);
1101
                            result.addDeletedObject(node);
1102
                        } else {
1103
                        	result.setError();
1104
                        	result.addException(new Exception("The taxon can not be deleted because it is not used in defined classification."));
1105
                        }
1106
                    } else if (config.isDeleteInAllClassifications()){
1107
                        List<TaxonNode> nodesList = new ArrayList<>();
1108
                        nodesList.addAll(taxon.getTaxonNodes());
1109
                        for (ITaxonTreeNode treeNode: nodesList){
1110
                            TaxonNode taxonNode = (TaxonNode) treeNode;
1111
                            if(!deleteChildren){
1112
                                Object[] childNodes = taxonNode.getChildNodes().toArray();
1113
                                for (Object childNode: childNodes){
1114
                                    TaxonNode childNodeCast = (TaxonNode) childNode;
1115
                                    taxonNode.getParent().addChildNode(childNodeCast, childNodeCast.getReference(), childNodeCast.getMicroReference());
1116
                                }
1117
                            }
1118
                        }
1119
                        config.getTaxonNodeConfig().setDeleteElement(false);
1120
                        DeleteResult resultNodes = nodeService.deleteTaxonNodes(nodesList, config);
1121
                        if (!resultNodes.isOk()){
1122
                        	result.addExceptions(resultNodes.getExceptions());
1123
                        	result.setStatus(resultNodes.getStatus());
1124
                        } else {
1125
                            result.addUpdatedObjects(resultNodes.getUpdatedObjects());
1126
                        }
1127
                    }
1128
                    if (!success){
1129
                        result.setError();
1130
                        result.addException(new Exception("The taxon can not be deleted because the taxon node can not be removed."));
1131
                    }
1132
                }
1133
             }
1134
             TaxonName name = taxon.getName();
1135
             taxon.setName(null);
1136
             this.saveOrUpdate(taxon);
1137

    
1138
             if ((taxon.getTaxonNodes() == null || taxon.getTaxonNodes().size()== 0)  && result.isOk()){
1139
                 try{
1140
                     dao.delete(taxon);
1141
                     result.addDeletedObject(taxon);
1142
                 }catch(Exception e){
1143
                     result.addException(e);
1144
                     result.setError();
1145
                 }
1146
             } else {
1147
                 result.setError();
1148
                 result.addException(new Exception("The Taxon can't be deleted because it is used in a classification."));
1149

    
1150
             }
1151
             //TaxonName
1152
             if (config.isDeleteNameIfPossible() && result.isOk()){
1153
                 DeleteResult nameResult = new DeleteResult();
1154
                 //remove name if possible (and required)
1155
                 if (name != null ){
1156
                     nameResult = nameService.delete(name.getUuid(), config.getNameDeletionConfig());
1157
                 }
1158
                 if (nameResult.isError() || nameResult.isAbort()){
1159
                     result.addRelatedObject(name);
1160
                     result.addExceptions(nameResult.getExceptions());
1161
                 }else{
1162
                     result.includeResult(nameResult);
1163
                 }
1164
             }
1165
       }
1166

    
1167
       return result;
1168
    }
1169

    
1170
    @Override
1171
    @Transactional(readOnly = false)
1172
    public DeleteResult delete(UUID synUUID){
1173
    	Synonym syn = (Synonym)dao.load(synUUID);
1174
        return this.deleteSynonym(syn, null);
1175
    }
1176

    
1177
    @Override
1178
    @Transactional(readOnly = false)
1179
    public DeleteResult deleteSynonym(UUID synonymUuid, SynonymDeletionConfigurator config) {
1180
        return deleteSynonym((Synonym)dao.load(synonymUuid), config);
1181
    }
1182

    
1183
    @Override
1184
    @Transactional(readOnly = false)
1185
    public DeleteResult deleteSynonym(Synonym synonym, SynonymDeletionConfigurator config) {
1186
        DeleteResult result = new DeleteResult();
1187
    	if (synonym == null){
1188
    		result.setAbort();
1189
    		result.addException(new Exception("The synonym was already deleted."));
1190
    		return result;
1191
        }
1192

    
1193
        if (config == null){
1194
            config = new SynonymDeletionConfigurator();
1195
        }
1196

    
1197
        result = isDeletable(synonym.getUuid(), config);
1198

    
1199
        if (result.isOk()){
1200

    
1201
            synonym = HibernateProxyHelper.deproxy(this.load(synonym.getUuid()), Synonym.class);
1202

    
1203
            //remove synonym
1204
            Taxon accTaxon = synonym.getAcceptedTaxon();
1205

    
1206
            if (accTaxon != null){
1207
                accTaxon = HibernateProxyHelper.deproxy(accTaxon, Taxon.class);
1208
                accTaxon.removeSynonym(synonym, false);
1209
                this.saveOrUpdate(accTaxon);
1210
                result.addUpdatedObject(accTaxon);
1211
            }
1212
            this.saveOrUpdate(synonym);
1213
            //#6281
1214
            dao.flush();
1215

    
1216
            TaxonName name = synonym.getName();
1217
            synonym.setName(null);
1218

    
1219
            dao.delete(synonym);
1220
            result.addDeletedObject(synonym);
1221

    
1222
            //remove name if possible (and required)
1223
            if (name != null && config.isDeleteNameIfPossible()){
1224

    
1225
                    DeleteResult nameDeleteResult = nameService.delete(name, config.getNameDeletionConfig());
1226
                    if (nameDeleteResult.isAbort() || nameDeleteResult.isError()){
1227
                    	result.addExceptions(nameDeleteResult.getExceptions());
1228
                    	result.addRelatedObject(name);
1229
                    }else{
1230
                        result.addDeletedObject(name);
1231
                    }
1232
            }
1233
        }
1234
        return result;
1235
    }
1236

    
1237
    @Override
1238
    public Map<String, Map<UUID,Set<TaxonName>>> findIdenticalTaxonNames(List<UUID> sourceRefUuids, List<String> propertyPaths) {
1239
        return this.dao.findIdenticalNames(sourceRefUuids, propertyPaths);
1240
    }
1241

    
1242
    @Override
1243
    public Taxon findBestMatchingTaxon(String taxonName) {
1244
        MatchingTaxonConfigurator config = MatchingTaxonConfigurator.NewInstance();
1245
        config.setTaxonNameTitle(taxonName);
1246
        return findBestMatchingTaxon(config);
1247
    }
1248

    
1249
    @Override
1250
    public Taxon findBestMatchingTaxon(MatchingTaxonConfigurator config) {
1251

    
1252
        Taxon bestCandidate = null;
1253
        try{
1254
            // 1. search for accepted taxa
1255
            List<TaxonBase> taxonList = dao.findByNameTitleCache(true, false, config.isIncludeUnpublished(),
1256
                    config.getTaxonNameTitle(), null, null, MatchMode.EXACT, null, null, 0, null, null);
1257
            boolean bestCandidateMatchesSecUuid = false;
1258
            boolean bestCandidateIsInClassification = false;
1259
            int countEqualCandidates = 0;
1260
            for(TaxonBase<?> taxonBaseCandidate : taxonList){
1261
                if(taxonBaseCandidate instanceof Taxon){
1262
                    Taxon newCanditate = CdmBase.deproxy(taxonBaseCandidate, Taxon.class);
1263
                    boolean newCandidateMatchesSecUuid = isMatchesSecUuid(newCanditate, config);
1264
                    if (! newCandidateMatchesSecUuid && config.isOnlyMatchingSecUuid() ){
1265
                        continue;
1266
                    }else if(newCandidateMatchesSecUuid && ! bestCandidateMatchesSecUuid){
1267
                        bestCandidate = newCanditate;
1268
                        countEqualCandidates = 1;
1269
                        bestCandidateMatchesSecUuid = true;
1270
                        continue;
1271
                    }
1272

    
1273
                    boolean newCandidateInClassification = isInClassification(newCanditate, config);
1274
                    if (! newCandidateInClassification && config.isOnlyMatchingClassificationUuid()){
1275
                        continue;
1276
                    }else if (newCandidateInClassification && ! bestCandidateIsInClassification){
1277
                        bestCandidate = newCanditate;
1278
                        countEqualCandidates = 1;
1279
                        bestCandidateIsInClassification = true;
1280
                        continue;
1281
                    }
1282
                    if (bestCandidate == null){
1283
                        bestCandidate = newCanditate;
1284
                        countEqualCandidates = 1;
1285
                        continue;
1286
                    }
1287
                }else{  //not Taxon.class
1288
                    continue;
1289
                }
1290
                countEqualCandidates++;
1291

    
1292
            }
1293
            if (bestCandidate != null){
1294
                if(countEqualCandidates > 1){
1295
                    logger.info(countEqualCandidates + " equally matching TaxonBases found, using first accepted Taxon: " + bestCandidate.getTitleCache());
1296
                    return bestCandidate;
1297
                } else {
1298
                    logger.info("using accepted Taxon: " + bestCandidate.getTitleCache());
1299
                    return bestCandidate;
1300
                }
1301
            }
1302

    
1303
            // 2. search for synonyms
1304
            if (config.isIncludeSynonyms()){
1305
                List<TaxonBase> synonymList = dao.findByNameTitleCache(false, true, config.isIncludeUnpublished(),
1306
                        config.getTaxonNameTitle(), null, null, MatchMode.EXACT, null, null, 0, null, null);
1307
                for(TaxonBase taxonBase : synonymList){
1308
                    if(taxonBase instanceof Synonym){
1309
                        Synonym synonym = CdmBase.deproxy(taxonBase, Synonym.class);
1310
                        bestCandidate = synonym.getAcceptedTaxon();
1311
                        if(bestCandidate != null){
1312
                            logger.info("using accepted Taxon " +  bestCandidate.getTitleCache() + " for synonym " + taxonBase.getTitleCache());
1313
                            return bestCandidate;
1314
                        }
1315
                        //TODO extend method: search using treeUUID, using SecUUID, first find accepted then include synonyms until a matching taxon is found
1316
                    }
1317
                }
1318
            }
1319

    
1320
        } catch (Exception e){
1321
            logger.error(e);
1322
            e.printStackTrace();
1323
        }
1324

    
1325
        return bestCandidate;
1326
    }
1327

    
1328
    private boolean isInClassification(Taxon taxon, MatchingTaxonConfigurator config) {
1329
        UUID configClassificationUuid = config.getClassificationUuid();
1330
        if (configClassificationUuid == null){
1331
            return false;
1332
        }
1333
        for (TaxonNode node : taxon.getTaxonNodes()){
1334
            UUID classUuid = node.getClassification().getUuid();
1335
            if (configClassificationUuid.equals(classUuid)){
1336
                return true;
1337
            }
1338
        }
1339
        return false;
1340
    }
1341

    
1342
    private boolean isMatchesSecUuid(Taxon taxon, MatchingTaxonConfigurator config) {
1343
        UUID configSecUuid = config.getSecUuid();
1344
        if (configSecUuid == null){
1345
            return false;
1346
        }
1347
        UUID taxonSecUuid = (taxon.getSec() == null)? null : taxon.getSec().getUuid();
1348
        return configSecUuid.equals(taxonSecUuid);
1349
    }
1350

    
1351
    @Override
1352
    public Synonym findBestMatchingSynonym(String taxonName, boolean includeUnpublished) {
1353
        List<TaxonBase> synonymList = dao.findByNameTitleCache(false, true, includeUnpublished, taxonName, null, null, MatchMode.EXACT, null, null, 0, null, null);
1354
        if(! synonymList.isEmpty()){
1355
            Synonym result = CdmBase.deproxy(synonymList.iterator().next(), Synonym.class);
1356
            if(synonymList.size() == 1){
1357
                logger.info(synonymList.size() + " Synonym found " + result.getTitleCache() );
1358
                return result;
1359
            } else {
1360
                logger.info("Several matching synonyms found. Using first: " +  result.getTitleCache());
1361
                return result;
1362
            }
1363
        }
1364
        return null;
1365
    }
1366

    
1367
    @Override
1368
    @Transactional(readOnly = false)
1369
    public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym,
1370
            Taxon newTaxon,
1371
            boolean moveHomotypicGroup,
1372
            SynonymType newSynonymType) throws HomotypicalGroupChangeException {
1373
        return moveSynonymToAnotherTaxon(oldSynonym, newTaxon, moveHomotypicGroup,
1374
                newSynonymType,
1375
                oldSynonym.getSec(),
1376
                oldSynonym.getSecMicroReference(),
1377
                true);
1378
    }
1379

    
1380
    @Override
1381
    @Transactional(readOnly = false)
1382
    public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym,
1383
            Taxon newTaxon,
1384
            boolean moveHomotypicGroup,
1385
            SynonymType newSynonymType,
1386
            Reference newSecundum,
1387
            String newSecundumDetail,
1388
            boolean keepSecundumIfUndefined) throws HomotypicalGroupChangeException {
1389

    
1390
        Synonym synonym = CdmBase.deproxy(dao.load(oldSynonym.getUuid()), Synonym.class);
1391
        Taxon oldTaxon = CdmBase.deproxy(dao.load(synonym.getAcceptedTaxon().getUuid()), Taxon.class);
1392
        //TODO what if there is no name ?? Concepts may be cached (e.g. via TCS import)
1393
        TaxonName synonymName = synonym.getName();
1394
        TaxonName fromTaxonName = oldTaxon.getName();
1395
        //set default relationship type
1396
        if (newSynonymType == null){
1397
            newSynonymType = SynonymType.HETEROTYPIC_SYNONYM_OF();
1398
        }
1399
        boolean newRelTypeIsHomotypic = newSynonymType.equals(SynonymType.HOMOTYPIC_SYNONYM_OF());
1400

    
1401
        HomotypicalGroup homotypicGroup = synonymName.getHomotypicalGroup();
1402
        int hgSize = homotypicGroup.getTypifiedNames().size();
1403
        boolean isSingleInGroup = !(hgSize > 1);
1404

    
1405
        if (! isSingleInGroup){
1406
            boolean isHomotypicToAccepted = synonymName.isHomotypic(fromTaxonName);
1407
            boolean hasHomotypicSynonymRelatives = isHomotypicToAccepted ? hgSize > 2 : hgSize > 1;
1408
            if (isHomotypicToAccepted){
1409
                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.";
1410
                String homotypicRelatives = hasHomotypicSynonymRelatives ? " and other synonym(s)":"";
1411
                message = String.format(message, homotypicRelatives);
1412
                throw new HomotypicalGroupChangeException(message);
1413
            }
1414
            if (! moveHomotypicGroup){
1415
                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.";
1416
                throw new HomotypicalGroupChangeException(message);
1417
            }
1418
        }else{
1419
            moveHomotypicGroup = true;  //single synonym always allows to moveCompleteGroup
1420
        }
1421
//        Assert.assertTrue("Synonym can only be moved with complete homotypic group", moveHomotypicGroup);
1422

    
1423
        UpdateResult result = new UpdateResult();
1424
        //move all synonyms to new taxon
1425
        List<Synonym> homotypicSynonyms = oldTaxon.getSynonymsInGroup(homotypicGroup);
1426
        for (Synonym synRelation: homotypicSynonyms){
1427

    
1428
            newTaxon = HibernateProxyHelper.deproxy(newTaxon, Taxon.class);
1429
            oldTaxon = HibernateProxyHelper.deproxy(oldTaxon, Taxon.class);
1430
            oldTaxon.removeSynonym(synRelation, false);
1431
            newTaxon.addSynonym(synRelation, newSynonymType);
1432

    
1433
            if (newSecundum != null || !keepSecundumIfUndefined){
1434
                synRelation.setSec(newSecundum);
1435
            }
1436
            if (newSecundumDetail != null || !keepSecundumIfUndefined){
1437
                synRelation.setSecMicroReference(newSecundumDetail);
1438
            }
1439

    
1440
            //set result  //why is this needed? Seems wrong to me (AM 10.10.2016)
1441
            if (!synRelation.equals(oldSynonym)){
1442
                result.setError();
1443
            }
1444
        }
1445

    
1446
        result.addUpdatedObject(oldTaxon);
1447
        result.addUpdatedObject(newTaxon);
1448
        saveOrUpdate(oldTaxon);
1449
        saveOrUpdate(newTaxon);
1450

    
1451
        return result;
1452
    }
1453

    
1454
    @Override
1455
    public <T extends TaxonBase>List<UuidAndTitleCache<T>> getUuidAndTitleCache(Class<T> clazz, Integer limit, String pattern) {
1456

    
1457
        return dao.getUuidAndTitleCache(clazz, limit, pattern);
1458
    }
1459

    
1460
    @Override
1461
    public Pager<SearchResult<TaxonBase>> findByFullText(
1462
            Class<? extends TaxonBase> clazz, String queryString,
1463
            Classification classification, TaxonNode subtree, boolean includeUnpublished, List<Language> languages,
1464
            boolean highlightFragments, Integer pageSize, Integer pageNumber,
1465
            List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
1466

    
1467
        LuceneSearch luceneSearch = prepareFindByFullTextSearch(clazz, queryString, classification, subtree,
1468
                null, includeUnpublished, languages, highlightFragments, null);
1469

    
1470
        // --- execute search
1471
        TopGroups<BytesRef> topDocsResultSet;
1472
        try {
1473
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
1474
        } catch (ParseException e) {
1475
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
1476
            luceneParseException.setStackTrace(e.getStackTrace());
1477
            throw luceneParseException;
1478
        }
1479

    
1480
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1481
        idFieldMap.put(CdmBaseType.TAXON, "id");
1482

    
1483
        // ---  initialize taxa, thighlight matches ....
1484
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1485
        @SuppressWarnings("rawtypes")
1486
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1487
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1488

    
1489
        long totalHits = topDocsResultSet != null ? Long.valueOf(topDocsResultSet.totalGroupCount) : 0;
1490
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
1491
    }
1492

    
1493
    @Transactional(readOnly = true)
1494
    @Override
1495
    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) {
1496
         long numberOfResults = dao.countByTitleWithRestrictions(clazz, queryString, matchmode, restrictions);
1497

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

    
1502
             results = dao.findByTitleWithRestrictions(clazz, queryString, matchmode, restrictions, pageSize, pageNumber, orderHints, propertyPaths);
1503
             results.addAll(dao.findByTitleWithRestrictions(clazz, "?".concat(queryString), matchmode, restrictions, pageSize, pageNumber, orderHints, propertyPaths));
1504
         }
1505
         Collections.sort(results, new TaxonComparator());
1506
         return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
1507
    }
1508

    
1509
    @Transactional(readOnly = true)
1510
    @Override
1511
    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) {
1512
        long numberOfResults = dao.countByTitle(clazz, queryString, matchmode, criteria);
1513
        //check whether there are doubtful taxa matching
1514
        long numberOfResults_doubtful = dao.countByTitle(clazz, "?".concat(queryString), matchmode, criteria);
1515
        List<S> results = new ArrayList<>();
1516
        if(numberOfResults > 0 || numberOfResults_doubtful > 0) { // no point checking again //TODO use AbstractPagerImpl.hasResultsInRange(numberOfResults, pageNumber, pageSize)
1517
               if (numberOfResults > 0){
1518
                   results = dao.findByTitle(clazz, queryString, matchmode, criteria, pageSize, pageNumber, orderHints, propertyPaths);
1519
               }else{
1520
                   results = new ArrayList<>();
1521
               }
1522
               if (numberOfResults_doubtful > 0){
1523
                   results.addAll(dao.findByTitle(clazz, "?".concat(queryString), matchmode,  criteria, pageSize, pageNumber, orderHints, propertyPaths));
1524
               }
1525
        }
1526
        Collections.sort(results, new TaxonComparator());
1527
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
1528
    }
1529

    
1530
    @Override
1531
    public Pager<SearchResult<TaxonBase>> findByDistribution(List<NamedArea> areaFilter, List<PresenceAbsenceTerm> statusFilter,
1532
            Classification classification, TaxonNode subtree,
1533
            Integer pageSize, Integer pageNumber,
1534
            List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
1535

    
1536
        LuceneSearch luceneSearch = prepareByDistributionSearch(areaFilter, statusFilter, classification, subtree);
1537

    
1538
        // --- execute search
1539
        TopGroups<BytesRef> topDocsResultSet;
1540
        try {
1541
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
1542
        } catch (ParseException e) {
1543
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
1544
            luceneParseException.setStackTrace(e.getStackTrace());
1545
            throw luceneParseException;
1546
        }
1547

    
1548
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1549
        idFieldMap.put(CdmBaseType.TAXON, "id");
1550

    
1551
        // ---  initialize taxa, thighlight matches ....
1552
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1553
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1554
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1555

    
1556
        long totalHits = topDocsResultSet != null ? Long.valueOf(topDocsResultSet.totalGroupCount) : 0;
1557
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
1558
    }
1559

    
1560
    /**
1561
     * @param clazz
1562
     * @param queryString
1563
     * @param classification
1564
     * @param includeUnpublished
1565
     * @param languages
1566
     * @param highlightFragments
1567
     * @param sortFields TODO
1568
     * @param directorySelectClass
1569
     * @return
1570
     */
1571
    protected LuceneSearch prepareFindByFullTextSearch(Class<? extends CdmBase> clazz, String queryString,
1572
            Classification classification, TaxonNode subtree, String className, boolean includeUnpublished, List<Language> languages,
1573
            boolean highlightFragments, SortField[] sortFields) {
1574

    
1575
        Builder finalQueryBuilder = new Builder();
1576
        Builder textQueryBuilder = new Builder();
1577

    
1578
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1579
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1580

    
1581
        if(sortFields == null){
1582
            sortFields = new SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING, false)};
1583
        }
1584
        luceneSearch.setSortFields(sortFields);
1585

    
1586
        // ---- search criteria
1587
        luceneSearch.setCdmTypRestriction(clazz);
1588

    
1589
        if(!StringUtils.isEmpty(queryString) && !queryString.equals("*") && !queryString.equals("?") ) {
1590
            textQueryBuilder.add(taxonBaseQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
1591
            textQueryBuilder.add(taxonBaseQueryFactory.newDefinedTermQuery("name.rank", queryString, languages), Occur.SHOULD);
1592
        }
1593
        if(className != null){
1594
            textQueryBuilder.add(taxonBaseQueryFactory.newTermQuery("classInfo.name", className, false), Occur.MUST);
1595
        }
1596

    
1597
        BooleanQuery textQuery = textQueryBuilder.build();
1598
        if(textQuery.clauses().size() > 0) {
1599
            finalQueryBuilder.add(textQuery, Occur.MUST);
1600
        }
1601

    
1602
        if(classification != null){
1603
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery(AcceptedTaxonBridge.DOC_KEY_CLASSIFICATION_ID, classification), Occur.MUST);
1604
        }
1605
        if(subtree != null){
1606
            finalQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(AcceptedTaxonBridge.DOC_KEY_TREEINDEX, subtree.treeIndexWc(), true), Occur.MUST);
1607
        }
1608
        if(!includeUnpublished)  {
1609
            String accPublishParam = TaxonBase.ACC_TAXON_BRIDGE_PREFIX + AcceptedTaxonBridge.DOC_KEY_PUBLISH_SUFFIX;
1610
            finalQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery(accPublishParam, true), Occur.MUST);
1611
            finalQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery("publish", true), Occur.MUST);
1612
        }
1613

    
1614
        luceneSearch.setQuery(finalQueryBuilder.build());
1615

    
1616
        if(highlightFragments){
1617
            luceneSearch.setHighlightFields(taxonBaseQueryFactory.getTextFieldNamesAsArray());
1618
        }
1619
        return luceneSearch;
1620
    }
1621

    
1622
    /**
1623
     * Uses org.apache.lucene.search.join.JoinUtil for query time joining, alternatively
1624
     * the BlockJoinQuery could be used. The latter might be more memory save but has the
1625
     * drawback of requiring to do the join an indexing time.
1626
     * see  http://dev.e-taxonomy.eu/trac/wiki/LuceneNotes#JoinsinLucene for more information on this.
1627
     *
1628
     * Joins TaxonRelationShip with Taxon depending on the direction of the given edge:
1629
     * <ul>
1630
     * <li>direct, everted: {@link Direction.relatedTo}: TaxonRelationShip.relatedTo.id --&gt; Taxon.id </li>
1631
     * <li>inverse: {@link Direction.relatedFrom}:  TaxonRelationShip.relatedFrom.id --&gt; Taxon.id </li>
1632
     * <ul>
1633
     * @param queryString
1634
     * @param classification
1635
     * @param languages
1636
     * @param highlightFragments
1637
     * @param sortFields TODO
1638
     *
1639
     * @return
1640
     * @throws IOException
1641
     */
1642
    protected LuceneSearch prepareFindByTaxonRelationFullTextSearch(TaxonRelationshipEdge edge, String queryString,
1643
            Classification classification, TaxonNode subtree, boolean includeUnpublished, List<Language> languages,
1644
            boolean highlightFragments, SortField[] sortFields) throws IOException {
1645

    
1646
        String fromField;
1647
        String queryTermField;
1648
        String toField = "id"; // TaxonBase.uuid
1649
        String publishField;
1650
        String publishFieldInvers;
1651

    
1652
        if(edge.isBidirectional()){
1653
            throw new RuntimeException("Bidirectional joining not supported!");
1654
        }
1655
        if(edge.isEvers()){
1656
            fromField = "relatedFrom.id";
1657
            queryTermField = "relatedFrom.titleCache";
1658
            publishField = "relatedFrom.publish";
1659
            publishFieldInvers = "relatedTo.publish";
1660
        } else if(edge.isInvers()) {
1661
            fromField = "relatedTo.id";
1662
            queryTermField = "relatedTo.titleCache";
1663
            publishField = "relatedTo.publish";
1664
            publishFieldInvers = "relatedFrom.publish";
1665
        } else {
1666
            throw new RuntimeException("Invalid direction: " + edge.getDirections());
1667
        }
1668

    
1669
        Builder finalQueryBuilder = new Builder();
1670

    
1671
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1672
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1673

    
1674
        Builder joinFromQueryBuilder = new Builder();
1675
        if(!StringUtils.isEmpty(queryString)){
1676
            joinFromQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(queryTermField, queryString), Occur.MUST);
1677
        }
1678
        joinFromQueryBuilder.add(taxonBaseQueryFactory.newEntityIdsQuery("type.id", edge.getRelationshipTypes()), Occur.MUST);
1679
        if(!includeUnpublished){
1680
            joinFromQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery(publishField, true), Occur.MUST);
1681
            joinFromQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery(publishFieldInvers, true), Occur.MUST);
1682
        }
1683

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

    
1686
        if(sortFields == null){
1687
            sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING,  false)};
1688
        }
1689
        luceneSearch.setSortFields(sortFields);
1690

    
1691
        finalQueryBuilder.add(joinQuery, Occur.MUST);
1692

    
1693
        if(classification != null){
1694
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery(AcceptedTaxonBridge.DOC_KEY_CLASSIFICATION_ID, classification), Occur.MUST);
1695
        }
1696
        if(subtree != null){
1697
            finalQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(AcceptedTaxonBridge.DOC_KEY_TREEINDEX, subtree.treeIndexWc(), true), Occur.MUST);
1698
        }
1699

    
1700
        luceneSearch.setQuery(finalQueryBuilder.build());
1701

    
1702
        if(highlightFragments){
1703
            luceneSearch.setHighlightFields(taxonBaseQueryFactory.getTextFieldNamesAsArray());
1704
        }
1705
        return luceneSearch;
1706
    }
1707

    
1708
    @Override
1709
    public Pager<SearchResult<TaxonBase>> findTaxaAndNamesByFullText(
1710
            EnumSet<TaxaAndNamesSearchMode> searchModes, String queryString,
1711
            Classification classification, TaxonNode subtree,
1712
            Set<NamedArea> namedAreas, Set<PresenceAbsenceTerm> distributionStatus, List<Language> languages,
1713
            boolean highlightFragments, Integer pageSize,
1714
            Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths)
1715
            throws IOException, LuceneParseException, LuceneMultiSearchException {
1716

    
1717
        // FIXME: allow taxonomic ordering
1718
        //  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";
1719
        // this require building a special sort column by a special classBridge
1720
        if(highlightFragments){
1721
            logger.warn("findTaxaAndNamesByFullText() : fragment highlighting is " +
1722
                    "currently not fully supported by this method and thus " +
1723
                    "may not work with common names and misapplied names.");
1724
        }
1725

    
1726
        // convert sets to lists
1727
        List<NamedArea> namedAreaList = null;
1728
        List<PresenceAbsenceTerm> distributionStatusList = null;
1729
        if(namedAreas != null){
1730
            namedAreaList = new ArrayList<>(namedAreas.size());
1731
            namedAreaList.addAll(namedAreas);
1732
        }
1733
        if(distributionStatus != null){
1734
            distributionStatusList = new ArrayList<>(distributionStatus.size());
1735
            distributionStatusList.addAll(distributionStatus);
1736
        }
1737

    
1738
        // set default if parameter is null
1739
        if(searchModes == null){
1740
            searchModes = EnumSet.of(TaxaAndNamesSearchMode.doTaxa);
1741
        }
1742

    
1743
        // set sort order and thus override any sort orders which may have been
1744
        // defined by prepare*Search methods
1745
        if(orderHints == null){
1746
            orderHints = OrderHint.NOMENCLATURAL_SORT_ORDER.asList();
1747
        }
1748
        SortField[] sortFields = new SortField[orderHints.size()];
1749
        int i = 0;
1750
        for(OrderHint oh : orderHints){
1751
            sortFields[i++] = oh.toSortField();
1752
        }
1753
//        SortField[] sortFields = new SortField[]{SortField.FIELD_SCORE, new SortField("id", SortField.STRING, false)};
1754
//        SortField[] sortFields = new SortField[]{new SortField(NomenclaturalSortOrderBrigde.NAME_SORT_FIELD_NAME, SortField.STRING, false)};
1755

    
1756
        boolean addDistributionFilter = namedAreas != null && namedAreas.size() > 0;
1757

    
1758
        List<LuceneSearch> luceneSearches = new ArrayList<>();
1759
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1760

    
1761
        /*
1762
          ======== filtering by distribution , HOWTO ========
1763

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

    
1769

    
1770
          3. how does it work in spatial?
1771
          see
1772
           - http://www.nsshutdown.com/projects/lucene/whitepaper/locallucene_v2.html
1773
           - http://www.infoq.com/articles/LuceneSpatialSupport
1774
           - http://www.mhaller.de/archives/156-Spatial-search-with-Lucene.html
1775
          ------------------------------------------------------------------------
1776

    
1777
          filter strategies:
1778
          A) use a separate distribution filter per index sub-query/search:
1779
           - byTaxonSyonym (query TaxaonBase):
1780
               use a join area filter (Distribution -> TaxonBase)
1781
           - byCommonName (query DescriptionElementBase): use an area filter on
1782
               DescriptionElementBase !!! PROBLEM !!!
1783
               This cannot work since the distributions are different entities than the
1784
               common names and thus these are different lucene documents.
1785
           - byMisaplliedNames (join query TaxonRelationship -> TaxonBase):
1786
               use a join area filter (Distribution -> TaxonBase)
1787

    
1788
          B) use a common distribution filter for all index sub-query/searches:
1789
           - use a common join area filter (Distribution -> TaxonBase)
1790
           - also implement the byCommonName as join query (CommonName -> TaxonBase)
1791
           PROBLEM in this case: we are losing the fragment highlighting for the
1792
           common names, since the returned documents are always TaxonBases
1793
        */
1794

    
1795
        /* The QueryFactory for creating filter queries on Distributions should
1796
         * The query factory used for the common names query cannot be reused
1797
         * for this case, since we want to only record the text fields which are
1798
         * actually used in the primary query
1799
         */
1800
        QueryFactory distributionFilterQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Distribution.class);
1801

    
1802
        Builder multiIndexByAreaFilterBuilder = new Builder();
1803
        boolean includeUnpublished = searchModes.contains(TaxaAndNamesSearchMode.includeUnpublished);
1804

    
1805
        // search for taxa or synonyms
1806
        if(searchModes.contains(TaxaAndNamesSearchMode.doTaxa) || searchModes.contains(TaxaAndNamesSearchMode.doSynonyms) ) {
1807
            @SuppressWarnings("rawtypes")
1808
            Class<? extends TaxonBase> taxonBaseSubclass = TaxonBase.class;
1809
            String className = null;
1810
            if(searchModes.contains(TaxaAndNamesSearchMode.doTaxa) && !searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
1811
                taxonBaseSubclass = Taxon.class;
1812
            } else if (!searchModes.contains(TaxaAndNamesSearchMode.doTaxa) && searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)) {
1813
                className = "eu.etaxonomy.cdm.model.taxon.Synonym";
1814
            }
1815
            luceneSearches.add(prepareFindByFullTextSearch(taxonBaseSubclass,
1816
                    queryString, classification, subtree, className,
1817
                    includeUnpublished, languages, highlightFragments, sortFields));
1818
            idFieldMap.put(CdmBaseType.TAXON, "id");
1819
            /* A) does not work!!!!
1820
            if(addDistributionFilter){
1821
                // in this case we need a filter which uses a join query
1822
                // to get the TaxonBase documents for the DescriptionElementBase documents
1823
                // which are matching the areas in question
1824
                Query taxonAreaJoinQuery = createByDistributionJoinQuery(
1825
                        namedAreaList,
1826
                        distributionStatusList,
1827
                        distributionFilterQueryFactory
1828
                        );
1829
                multiIndexByAreaFilter.add(new QueryWrapperFilter(taxonAreaJoinQuery), Occur.SHOULD);
1830
            }
1831
            */
1832
            if(addDistributionFilter && searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
1833
                // add additional area filter for synonyms
1834
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1835
                String toField = "accTaxon" + AcceptedTaxonBridge.DOC_KEY_ID_SUFFIX; // id in TaxonBase index
1836

    
1837
                //TODO replace by createByDistributionJoinQuery
1838
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
1839
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class, fromField, true, byDistributionQuery, toField, Taxon.class, ScoreMode.None);
1840
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1841

    
1842
            }
1843
        }
1844

    
1845
        // search by CommonTaxonName
1846
        if(searchModes.contains(TaxaAndNamesSearchMode.doTaxaByCommonNames)) {
1847
            // B)
1848
            QueryFactory descriptionElementQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(DescriptionElementBase.class);
1849
            Query byCommonNameJoinQuery = descriptionElementQueryFactory.newJoinQuery(
1850
                    CommonTaxonName.class,
1851
                    "inDescription.taxon.id",
1852
                    true,
1853
                    QueryFactory.addTypeRestriction(
1854
                                createByDescriptionElementFullTextQuery(queryString, classification, subtree, null, languages, descriptionElementQueryFactory)
1855
                                , CommonTaxonName.class
1856
                                ).build(), "id", null, ScoreMode.Max);
1857
            if (logger.isDebugEnabled()){logger.debug("byCommonNameJoinQuery: " + byCommonNameJoinQuery.toString());}
1858
            LuceneSearch byCommonNameSearch = new LuceneSearch(luceneIndexToolProvider,
1859
                    GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
1860
            byCommonNameSearch.setCdmTypRestriction(Taxon.class);
1861
            Builder builder = new BooleanQuery.Builder();
1862
            builder.add(byCommonNameJoinQuery, Occur.MUST);
1863
            if(!includeUnpublished)  {
1864
                QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1865
                builder.add(taxonBaseQueryFactory.newBooleanQuery("publish", true), Occur.MUST);
1866
            }
1867
            byCommonNameSearch.setQuery(builder.build());
1868
            byCommonNameSearch.setSortFields(sortFields);
1869

    
1870
            idFieldMap.put(CdmBaseType.TAXON, "id");
1871

    
1872
            luceneSearches.add(byCommonNameSearch);
1873

    
1874
            /* A) does not work!!!!
1875
            luceneSearches.add(
1876
                    prepareByDescriptionElementFullTextSearch(CommonTaxonName.class,
1877
                            queryString, classification, null, languages, highlightFragments)
1878
                        );
1879
            idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
1880
            if(addDistributionFilter){
1881
                // in this case we are able to use DescriptionElementBase documents
1882
                // which are matching the areas in question directly
1883
                BooleanQuery byDistributionQuery = createByDistributionQuery(
1884
                        namedAreaList,
1885
                        distributionStatusList,
1886
                        distributionFilterQueryFactory
1887
                        );
1888
                multiIndexByAreaFilter.add(new QueryWrapperFilter(byDistributionQuery), Occur.SHOULD);
1889
            } */
1890
        }
1891

    
1892

    
1893
        // search by misapplied names
1894
        //TODO merge with pro parte synonym search once #7487 is fixed
1895
        if(searchModes.contains(TaxaAndNamesSearchMode.doMisappliedNames) /*|| searchModes.contains(TaxaAndNamesSearchMode.doSynonyms) */) {
1896
            // NOTE:
1897
            // prepareFindByTaxonRelationFullTextSearch() is making use of JoinUtil.createJoinQuery()
1898
            // which allows doing query time joins
1899
            // finds the misapplied name (Taxon B) which is an misapplication for
1900
            // a related Taxon A.
1901
            //
1902
            Set<TaxonRelationshipType> relTypes = new HashSet<>();
1903
            if (searchModes.contains(TaxaAndNamesSearchMode.doMisappliedNames)){
1904
                relTypes.addAll(TaxonRelationshipType.allMisappliedNameTypes());
1905
            }
1906
//            if (searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
1907
//                relTypes.addAll(TaxonRelationshipType.allSynonymTypes());
1908
//            }
1909

    
1910
            luceneSearches.add(prepareFindByTaxonRelationFullTextSearch(
1911
                    new TaxonRelationshipEdge(relTypes, Direction.relatedTo),
1912
                    queryString, classification, subtree, includeUnpublished, languages, highlightFragments, sortFields));
1913
            idFieldMap.put(CdmBaseType.TAXON, "id");
1914

    
1915
            if(addDistributionFilter){
1916
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1917

    
1918
                /*
1919
                 * Here I was facing a weird and nasty bug which took me bugging be really for hours until I found this solution.
1920
                 * Maybe this is a bug in java itself.
1921
                 *
1922
                 * When the string toField is constructed by using the expression TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString()
1923
                 * directly:
1924
                 *
1925
                 *    String toField = "relation." + TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString() +".to.id";
1926
                 *
1927
                 * The byDistributionQuery fails, however when the uuid is first stored in another string variable the query
1928
                 * will execute as expected:
1929
                 *
1930
                 *    String misappliedNameForUuid = TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString();
1931
                 *    String toField = "relation." + misappliedNameForUuid +".to.id";
1932
                 *
1933
                 * Comparing both strings by the String.equals method returns true, so both String are identical.
1934
                 *
1935
                 * The bug occurs when running eu.etaxonomy.cdm.api.service.TaxonServiceSearchTest in eclipse and in maven and seems to to be
1936
                 * 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)
1937
                 * The bug is persistent after a reboot of the development computer.
1938
                 */
1939
//                String misappliedNameForUuid = TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString();
1940
//                String toField = "relation." + misappliedNameForUuid +".to.id";
1941
                String toField = "relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id";
1942
//                System.out.println("relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id".equals("relation." + misappliedNameForUuid +".to.id") ? " > identical" : " > different");
1943
//                System.out.println("relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id".equals("relation." + TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString() +".to.id") ? " > identical" : " > different");
1944

    
1945
                //TODO replace by createByDistributionJoinQuery
1946
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
1947
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class,
1948
                        fromField, true, byDistributionQuery, toField, null, ScoreMode.None);
1949

    
1950
//                debug code for bug described above
1951
                //does not compile anymore since changing from lucene 3.6.2 to lucene 4.10+
1952
//                DocIdSet filterMatchSet = filter.getDocIdSet(luceneIndexToolProvider.getIndexReaderFor(Taxon.class));
1953
//                System.err.println(DocIdBitSetPrinter.docsAsString(filterMatchSet, 100));
1954

    
1955
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1956
            }
1957
        }
1958

    
1959
        // search by pro parte synonyms
1960
        if(searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)) {
1961
            //TODO merge with misapplied name search once #7487 is fixed
1962
            Set<TaxonRelationshipType> relTypes = new HashSet<>();
1963
            relTypes.addAll(TaxonRelationshipType.allSynonymTypes());
1964

    
1965
            luceneSearches.add(prepareFindByTaxonRelationFullTextSearch(
1966
                    new TaxonRelationshipEdge(relTypes, Direction.relatedTo),
1967
                    queryString, classification, subtree, includeUnpublished, languages, highlightFragments, sortFields));
1968
            idFieldMap.put(CdmBaseType.TAXON, "id");
1969

    
1970
            if(addDistributionFilter){
1971
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1972
                String toField = "relation.8a896603-0fa3-44c6-9cd7-df2d8792e577.to.id";
1973
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
1974
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class,
1975
                        fromField, true, byDistributionQuery, toField, null, ScoreMode.None);
1976
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1977
            }
1978
        }//end pro parte synonyms
1979

    
1980
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider,
1981
                luceneSearches.toArray(new LuceneSearch[luceneSearches.size()]));
1982

    
1983
        if(addDistributionFilter){
1984

    
1985
            // B)
1986
            // in this case we need a filter which uses a join query
1987
            // to get the TaxonBase documents for the DescriptionElementBase documents
1988
            // which are matching the areas in question
1989
            //
1990
            // for doTaxa, doByCommonName
1991
            Query taxonAreaJoinQuery = createByDistributionJoinQuery(
1992
                    namedAreaList,
1993
                    distributionStatusList,
1994
                    distributionFilterQueryFactory,
1995
                    Taxon.class, true
1996
                    );
1997
            multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1998
        }
1999

    
2000
        if (addDistributionFilter){
2001
            multiSearch.setFilter(multiIndexByAreaFilterBuilder.build());
2002
        }
2003

    
2004

    
2005
        // --- execute search
2006
        TopGroups<BytesRef> topDocsResultSet;
2007
        try {
2008
            topDocsResultSet = multiSearch.executeSearch(pageSize, pageNumber);
2009
        } catch (ParseException e) {
2010
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2011
            luceneParseException.setStackTrace(e.getStackTrace());
2012
            throw luceneParseException;
2013
        }
2014

    
2015
        // --- initialize taxa, highlight matches ....
2016
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
2017

    
2018

    
2019
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2020
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2021

    
2022
        long totalHits = (topDocsResultSet != null) ? Long.valueOf(topDocsResultSet.totalGroupCount) : 0;
2023
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
2024
    }
2025

    
2026
    /**
2027
     * @param namedAreaList at least one area must be in the list
2028
     * @param distributionStatusList optional
2029
     * @param toType toType
2030
     *      Optional parameter. Only used for debugging to print the toType documents
2031
     * @param asFilter TODO
2032
     * @return
2033
     * @throws IOException
2034
     */
2035
    protected Query createByDistributionJoinQuery(
2036
            List<NamedArea> namedAreaList,
2037
            List<PresenceAbsenceTerm> distributionStatusList,
2038
            QueryFactory queryFactory, Class<? extends CdmBase> toType, boolean asFilter
2039
            ) throws IOException {
2040

    
2041
        String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
2042
        String toField = "id"; // id in toType usually this is the TaxonBase index
2043

    
2044
        BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, queryFactory);
2045

    
2046
        ScoreMode scoreMode = asFilter ?  ScoreMode.None : ScoreMode.Max;
2047

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

    
2050
        return taxonAreaJoinQuery;
2051
    }
2052

    
2053
    /**
2054
     * @param namedAreaList
2055
     * @param distributionStatusList
2056
     * @param queryFactory
2057
     * @return
2058
     */
2059
    private BooleanQuery createByDistributionQuery(List<NamedArea> namedAreaList,
2060
            List<PresenceAbsenceTerm> distributionStatusList, QueryFactory queryFactory) {
2061
        Builder areaQueryBuilder = new Builder();
2062
        // area field from Distribution
2063
        areaQueryBuilder.add(queryFactory.newEntityIdsQuery("area.id", namedAreaList), Occur.MUST);
2064

    
2065
        // status field from Distribution
2066
        if(distributionStatusList != null && distributionStatusList.size() > 0){
2067
            areaQueryBuilder.add(queryFactory.newEntityIdsQuery("status.id", distributionStatusList), Occur.MUST);
2068
        }
2069

    
2070
        BooleanQuery areaQuery = areaQueryBuilder.build();
2071
        logger.debug("createByDistributionQuery() query: " + areaQuery.toString());
2072
        return areaQuery;
2073
    }
2074

    
2075
    /**
2076
     * This method has been primarily created for testing the area join query but might
2077
     * also be useful in other situations
2078
     *
2079
     * @param namedAreaList
2080
     * @param distributionStatusList
2081
     * @param classification
2082
     * @param highlightFragments
2083
     * @return
2084
     * @throws IOException
2085
     */
2086
    protected LuceneSearch prepareByDistributionSearch(
2087
            List<NamedArea> namedAreaList, List<PresenceAbsenceTerm> distributionStatusList,
2088
            Classification classification, TaxonNode subtree) throws IOException {
2089

    
2090
        Builder finalQueryBuilder = new Builder();
2091

    
2092
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
2093

    
2094
        // FIXME is this query factory using the wrong type?
2095
        QueryFactory taxonQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Taxon.class);
2096

    
2097
        SortField[] sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING, false)};
2098
        luceneSearch.setSortFields(sortFields);
2099

    
2100

    
2101
        Query byAreaQuery = createByDistributionJoinQuery(namedAreaList, distributionStatusList, taxonQueryFactory, null, false);
2102

    
2103
        finalQueryBuilder.add(byAreaQuery, Occur.MUST);
2104

    
2105
        if(classification != null){
2106
            finalQueryBuilder.add(taxonQueryFactory.newEntityIdQuery(AcceptedTaxonBridge.DOC_KEY_CLASSIFICATION_ID, classification), Occur.MUST);
2107
        }
2108
        if(subtree != null){
2109
            finalQueryBuilder.add(taxonQueryFactory.newTermQuery(AcceptedTaxonBridge.DOC_KEY_TREEINDEX, subtree.treeIndexWc(), true), Occur.MUST);
2110
        }
2111
        BooleanQuery finalQuery = finalQueryBuilder.build();
2112
        logger.info("prepareByAreaSearch() query: " + finalQuery.toString());
2113
        luceneSearch.setQuery(finalQuery);
2114

    
2115
        return luceneSearch;
2116
    }
2117

    
2118
    @Override
2119
    public Pager<SearchResult<TaxonBase>> findByDescriptionElementFullText(
2120
            Class<? extends DescriptionElementBase> clazz, String queryString,
2121
            Classification classification, TaxonNode subtree, List<Feature> features, List<Language> languages,
2122
            boolean highlightFragments, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
2123

    
2124
        LuceneSearch luceneSearch = prepareByDescriptionElementFullTextSearch(clazz, queryString, classification, subtree, features, languages, highlightFragments);
2125

    
2126
        // --- execute search
2127
        TopGroups<BytesRef> topDocsResultSet;
2128
        try {
2129
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
2130
        } catch (ParseException e) {
2131
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2132
            luceneParseException.setStackTrace(e.getStackTrace());
2133
            throw luceneParseException;
2134
        }
2135

    
2136
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
2137
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2138

    
2139
        // --- initialize taxa, highlight matches ....
2140
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
2141
        @SuppressWarnings("rawtypes")
2142
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2143
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2144

    
2145
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
2146
        return new DefaultPagerImpl<>(pageNumber, Long.valueOf(totalHits), pageSize, searchResults);
2147
    }
2148

    
2149
    @Override
2150
    public Pager<SearchResult<TaxonBase>> findByEverythingFullText(String queryString,
2151
            Classification classification, TaxonNode subtree, boolean includeUnpublished, List<Language> languages, boolean highlightFragments,
2152
            Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException, LuceneMultiSearchException {
2153

    
2154
        LuceneSearch luceneSearchByDescriptionElement = prepareByDescriptionElementFullTextSearch(null, queryString,
2155
                classification, subtree,
2156
                null, languages, highlightFragments);
2157
        LuceneSearch luceneSearchByTaxonBase = prepareFindByFullTextSearch(null, queryString, classification, subtree, null,
2158
                includeUnpublished, languages, highlightFragments, null);
2159

    
2160
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider, luceneSearchByDescriptionElement, luceneSearchByTaxonBase);
2161

    
2162
        // --- execute search
2163
        TopGroups<BytesRef> topDocsResultSet;
2164
        try {
2165
            topDocsResultSet = multiSearch.executeSearch(pageSize, pageNumber);
2166
        } catch (ParseException e) {
2167
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2168
            luceneParseException.setStackTrace(e.getStackTrace());
2169
            throw luceneParseException;
2170
        }
2171

    
2172
        // --- initialize taxa, highlight matches ....
2173
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
2174

    
2175
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
2176
        idFieldMap.put(CdmBaseType.TAXON, "id");
2177
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2178

    
2179
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2180
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2181

    
2182
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
2183
        return new DefaultPagerImpl<>(pageNumber, Long.valueOf(totalHits), pageSize, searchResults);
2184
    }
2185

    
2186
    /**
2187
     * @param clazz
2188
     * @param queryString
2189
     * @param classification
2190
     * @param features
2191
     * @param languages
2192
     * @param highlightFragments
2193
     * @param directorySelectClass
2194
     * @return
2195
     */
2196
    protected LuceneSearch prepareByDescriptionElementFullTextSearch(Class<? extends CdmBase> clazz,
2197
            String queryString, Classification classification, TaxonNode subtree, List<Feature> features,
2198
            List<Language> languages, boolean highlightFragments) {
2199

    
2200
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, DescriptionElementBase.class);
2201
        QueryFactory descriptionElementQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(DescriptionElementBase.class);
2202

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

    
2205
        BooleanQuery finalQuery = createByDescriptionElementFullTextQuery(queryString, classification, subtree, features,
2206
                languages, descriptionElementQueryFactory);
2207

    
2208
        luceneSearch.setSortFields(sortFields);
2209
        luceneSearch.setCdmTypRestriction(clazz);
2210
        luceneSearch.setQuery(finalQuery);
2211
        if(highlightFragments){
2212
            luceneSearch.setHighlightFields(descriptionElementQueryFactory.getTextFieldNamesAsArray());
2213
        }
2214

    
2215
        return luceneSearch;
2216
    }
2217

    
2218
    /**
2219
     * @param queryString
2220
     * @param classification
2221
     * @param features
2222
     * @param languages
2223
     * @param descriptionElementQueryFactory
2224
     * @return
2225
     */
2226
    private BooleanQuery createByDescriptionElementFullTextQuery(String queryString,
2227
            Classification classification, TaxonNode subtree, List<Feature> features,
2228
            List<Language> languages, QueryFactory descriptionElementQueryFactory) {
2229

    
2230
        Builder finalQueryBuilder = new Builder();
2231
        Builder textQueryBuilder = new Builder();
2232

    
2233
        if(!StringUtils.isEmpty(queryString)){
2234

    
2235
            textQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
2236

    
2237
            // common name
2238
            Builder nameQueryBuilder = new Builder();
2239
            if(languages == null || languages.size() == 0){
2240
                nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2241
            } else {
2242
                Builder languageSubQueryBuilder = new Builder();
2243
                for(Language lang : languages){
2244
                    languageSubQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("language.uuid",  lang.getUuid().toString(), false), Occur.SHOULD);
2245
                }
2246
                nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2247
                nameQueryBuilder.add(languageSubQueryBuilder.build(), Occur.MUST);
2248
            }
2249
            textQueryBuilder.add(nameQueryBuilder.build(), Occur.SHOULD);
2250

    
2251

    
2252
            // text field from TextData
2253
            textQueryBuilder.add(descriptionElementQueryFactory.newMultilanguageTextQuery("text", queryString, languages), Occur.SHOULD);
2254

    
2255
            // --- TermBase fields - by representation ----
2256
            // state field from CategoricalData
2257
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.state", queryString, languages), Occur.SHOULD);
2258

    
2259
            // state field from CategoricalData
2260
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.modifyingText", queryString, languages), Occur.SHOULD);
2261

    
2262
            // area field from Distribution
2263
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("area", queryString, languages), Occur.SHOULD);
2264

    
2265
            // status field from Distribution
2266
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("status", queryString, languages), Occur.SHOULD);
2267

    
2268
            finalQueryBuilder.add(textQueryBuilder.build(), Occur.MUST);
2269

    
2270
        }
2271
        // --- classification ----
2272

    
2273

    
2274
        if(classification != null){
2275
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityIdQuery("inDescription.taxon.taxonNodes.classification.id", classification), Occur.MUST);
2276
        }
2277
        if(subtree != null){
2278
            finalQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("inDescription.taxon.taxonNodes.treeIndex", subtree.treeIndexWc(), true), Occur.MUST);
2279
        }
2280

    
2281
        // --- IdentifieableEntity fields - by uuid
2282
        if(features != null && features.size() > 0 ){
2283
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityUuidsQuery("feature.uuid", features), Occur.MUST);
2284
        }
2285

    
2286
        // the description must be associated with a taxon
2287
        finalQueryBuilder.add(descriptionElementQueryFactory.newIsNotNullQuery("inDescription.taxon.id"), Occur.MUST);
2288

    
2289
        BooleanQuery finalQuery = finalQueryBuilder.build();
2290
        logger.info("prepareByDescriptionElementFullTextSearch() query: " + finalQuery.toString());
2291
        return finalQuery;
2292
    }
2293

    
2294
    @Override
2295
    public List<Synonym> createInferredSynonyms(Taxon taxon, Classification classification, SynonymType type, boolean doWithMisappliedNames){
2296

    
2297
        List <Synonym> inferredSynonyms = new ArrayList<>();
2298
        List<Synonym> inferredSynonymsToBeRemoved = new ArrayList<>();
2299

    
2300
        Map <UUID, IZoologicalName> zooHashMap = new HashMap<>();
2301
        boolean includeUnpublished = INCLUDE_UNPUBLISHED;
2302

    
2303
        UUID nameUuid= taxon.getName().getUuid();
2304
        IZoologicalName taxonName = getZoologicalName(nameUuid, zooHashMap);
2305
        String epithetOfTaxon = null;
2306
        String infragenericEpithetOfTaxon = null;
2307
        String infraspecificEpithetOfTaxon = null;
2308
        if (taxonName.isSpecies()){
2309
             epithetOfTaxon= taxonName.getSpecificEpithet();
2310
        } else if (taxonName.isInfraGeneric()){
2311
            infragenericEpithetOfTaxon = taxonName.getInfraGenericEpithet();
2312
        } else if (taxonName.isInfraSpecific()){
2313
            infraspecificEpithetOfTaxon = taxonName.getInfraSpecificEpithet();
2314
        }
2315
        String genusOfTaxon = taxonName.getGenusOrUninomial();
2316
        Set<TaxonNode> nodes = taxon.getTaxonNodes();
2317
        List<String> taxonNames = new ArrayList<>();
2318

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

    
2323
            if (node.getClassification().equals(classification)){
2324
                if (!node.isTopmostNode()){
2325
                    TaxonNode parent = node.getParent();
2326
                    parent = CdmBase.deproxy(parent);
2327
                    TaxonName parentName =  parent.getTaxon().getName();
2328
                    IZoologicalName zooParentName = CdmBase.deproxy(parentName);
2329
                    Taxon parentTaxon = CdmBase.deproxy(parent.getTaxon());
2330

    
2331
                    //create inferred synonyms for species, subspecies
2332
                    if ((parentName.isGenus() || parentName.isSpecies() || parentName.getRank().equals(Rank.SUBGENUS())) ){
2333

    
2334
                        Synonym inferredEpithet = null;
2335
                        Synonym inferredGenus = null;
2336
                        Synonym potentialCombination = null;
2337

    
2338
                        List<String> propertyPaths = new ArrayList<>();
2339
                        propertyPaths.add("synonym");
2340
                        propertyPaths.add("synonym.name");
2341
                        List<OrderHint> orderHintsSynonyms = new ArrayList<>();
2342
                        orderHintsSynonyms.add(new OrderHint("titleCache", SortOrder.ASCENDING));
2343

    
2344
                        List<Synonym> synonyMsOfParent = dao.getSynonyms(parentTaxon, SynonymType.HETEROTYPIC_SYNONYM_OF(), null, null,orderHintsSynonyms,propertyPaths);
2345
                        List<Synonym> synonymsOfTaxon= dao.getSynonyms(taxon, SynonymType.HETEROTYPIC_SYNONYM_OF(),
2346
                                null, null,orderHintsSynonyms,propertyPaths);
2347

    
2348
                        List<TaxonRelationship> taxonRelListParent = new ArrayList<>();
2349
                        List<TaxonRelationship> taxonRelListTaxon = new ArrayList<>();
2350
                        if (doWithMisappliedNames){
2351
                            List<OrderHint> orderHintsMisapplied = new ArrayList<>();
2352
                            orderHintsMisapplied.add(new OrderHint("relatedFrom.titleCache", SortOrder.ASCENDING));
2353
                            taxonRelListParent = dao.getTaxonRelationships(parentTaxon, TaxonRelationshipType.MISAPPLIED_NAME_FOR(),
2354
                                    includeUnpublished, null, null, orderHintsMisapplied, propertyPaths, Direction.relatedTo);
2355
                            taxonRelListTaxon = dao.getTaxonRelationships(taxon, TaxonRelationshipType.MISAPPLIED_NAME_FOR(),
2356
                                    includeUnpublished, null, null, orderHintsMisapplied, propertyPaths, Direction.relatedTo);
2357
                        }
2358

    
2359
                        if (type.equals(SynonymType.INFERRED_EPITHET_OF())){
2360
                            for (Synonym synonymRelationOfParent:synonyMsOfParent){
2361

    
2362
                                inferredEpithet = createInferredEpithets(taxon,
2363
                                        zooHashMap, taxonName, epithetOfTaxon,
2364
                                        infragenericEpithetOfTaxon,
2365
                                        infraspecificEpithetOfTaxon,
2366
                                        taxonNames, parentName,
2367
                                        synonymRelationOfParent);
2368

    
2369
                                inferredSynonyms.add(inferredEpithet);
2370
                                zooHashMap.put(inferredEpithet.getName().getUuid(), inferredEpithet.getName());
2371
                                taxonNames.add(inferredEpithet.getName().getNameCache());
2372
                            }
2373

    
2374
                            if (doWithMisappliedNames){
2375

    
2376
                                for (TaxonRelationship taxonRelationship: taxonRelListParent){
2377
                                     Taxon misappliedName = taxonRelationship.getFromTaxon();
2378

    
2379
                                     inferredEpithet = createInferredEpithets(taxon,
2380
                                             zooHashMap, taxonName, epithetOfTaxon,
2381
                                             infragenericEpithetOfTaxon,
2382
                                             infraspecificEpithetOfTaxon,
2383
                                             taxonNames, parentName,
2384
                                             misappliedName);
2385

    
2386
                                    inferredSynonyms.add(inferredEpithet);
2387
                                    zooHashMap.put(inferredEpithet.getName().getUuid(), inferredEpithet.getName());
2388
                                    taxonNames.add(inferredEpithet.getName().getNameCache());
2389
                                }
2390
                            }
2391

    
2392
                            if (!taxonNames.isEmpty()){
2393
                                List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2394
                                if (!synNotInCDM.isEmpty()){
2395
                                    inferredSynonymsToBeRemoved.clear();
2396

    
2397
                                    for (Synonym syn :inferredSynonyms){
2398
                                        IZoologicalName name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2399
                                        if (!synNotInCDM.contains(name.getNameCache())){
2400
                                            inferredSynonymsToBeRemoved.add(syn);
2401
                                        }
2402
                                    }
2403

    
2404
                                    // Remove identified Synonyms from inferredSynonyms
2405
                                    for (Synonym synonym : inferredSynonymsToBeRemoved) {
2406
                                        inferredSynonyms.remove(synonym);
2407
                                    }
2408
                                }
2409
                            }
2410

    
2411
                        }else if (type.equals(SynonymType.INFERRED_GENUS_OF())){
2412

    
2413
                            for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2414

    
2415
                                inferredGenus = createInferredGenus(taxon,
2416
                                        zooHashMap, taxonName, epithetOfTaxon,
2417
                                        genusOfTaxon, taxonNames, zooParentName, synonymRelationOfTaxon);
2418

    
2419
                                inferredSynonyms.add(inferredGenus);
2420
                                zooHashMap.put(inferredGenus.getName().getUuid(), inferredGenus.getName());
2421
                                taxonNames.add(inferredGenus.getName().getNameCache());
2422
                            }
2423

    
2424
                            if (doWithMisappliedNames){
2425

    
2426
                                for (TaxonRelationship taxonRelationship: taxonRelListTaxon){
2427
                                    Taxon misappliedName = taxonRelationship.getFromTaxon();
2428
                                    inferredGenus = createInferredGenus(taxon, zooHashMap, taxonName, infraspecificEpithetOfTaxon, genusOfTaxon, taxonNames, zooParentName,  misappliedName);
2429

    
2430
                                    inferredSynonyms.add(inferredGenus);
2431
                                    zooHashMap.put(inferredGenus.getName().getUuid(), inferredGenus.getName());
2432
                                     taxonNames.add(inferredGenus.getName().getNameCache());
2433
                                }
2434
                            }
2435

    
2436

    
2437
                            if (!taxonNames.isEmpty()){
2438
                                List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2439
                                IZoologicalName name;
2440
                                if (!synNotInCDM.isEmpty()){
2441
                                    inferredSynonymsToBeRemoved.clear();
2442

    
2443
                                    for (Synonym syn :inferredSynonyms){
2444
                                        name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2445
                                        if (!synNotInCDM.contains(name.getNameCache())){
2446
                                            inferredSynonymsToBeRemoved.add(syn);
2447
                                        }
2448
                                    }
2449

    
2450
                                    // Remove identified Synonyms from inferredSynonyms
2451
                                    for (Synonym synonym : inferredSynonymsToBeRemoved) {
2452
                                        inferredSynonyms.remove(synonym);
2453
                                    }
2454
                                }
2455
                            }
2456

    
2457
                        }else if (type.equals(SynonymType.POTENTIAL_COMBINATION_OF())){
2458

    
2459
                            Reference sourceReference = null; // TODO: Determination of sourceReference is redundant
2460
                            //for all synonyms of the parent...
2461
                            for (Synonym synonymRelationOfParent:synonyMsOfParent){
2462
                                TaxonName synName;
2463
                                HibernateProxyHelper.deproxy(synonymRelationOfParent);
2464

    
2465
                                synName = synonymRelationOfParent.getName();
2466

    
2467
                                // Set the sourceReference
2468
                                sourceReference = synonymRelationOfParent.getSec();
2469

    
2470
                                // Determine the idInSource
2471
                                String idInSourceParent = getIdInSource(synonymRelationOfParent);
2472

    
2473
                                IZoologicalName parentSynZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2474
                                String synParentGenus = parentSynZooName.getGenusOrUninomial();
2475
                                String synParentInfragenericName = null;
2476
                                String synParentSpecificEpithet = null;
2477

    
2478
                                if (parentSynZooName.isInfraGeneric()){
2479
                                    synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2480
                                }
2481
                                if (parentSynZooName.isSpecies()){
2482
                                    synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2483
                                }
2484

    
2485
                               /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2486
                                    synonymsGenus.put(synGenusName, idInSource);
2487
                                }*/
2488

    
2489
                                //for all synonyms of the taxon
2490

    
2491
                                for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2492

    
2493
                                    IZoologicalName zooSynName = getZoologicalName(synonymRelationOfTaxon.getName().getUuid(), zooHashMap);
2494
                                    potentialCombination = createPotentialCombination(idInSourceParent, parentSynZooName, zooSynName,
2495
                                            synParentGenus,
2496
                                            synParentInfragenericName,
2497
                                            synParentSpecificEpithet, synonymRelationOfTaxon, zooHashMap);
2498

    
2499
                                    taxon.addSynonym(potentialCombination, SynonymType.POTENTIAL_COMBINATION_OF());
2500
                                    inferredSynonyms.add(potentialCombination);
2501
                                    zooHashMap.put(potentialCombination.getName().getUuid(), potentialCombination.getName());
2502
                                     taxonNames.add(potentialCombination.getName().getNameCache());
2503
                                }
2504
                            }
2505

    
2506
                            if (doWithMisappliedNames){
2507

    
2508
                                for (TaxonRelationship parentRelationship: taxonRelListParent){
2509

    
2510
                                    TaxonName misappliedParentName;
2511

    
2512
                                    Taxon misappliedParent = parentRelationship.getFromTaxon();
2513
                                    misappliedParentName = misappliedParent.getName();
2514

    
2515
                                    HibernateProxyHelper.deproxy(misappliedParent);
2516

    
2517
                                    // Set the sourceReference
2518
                                    sourceReference = misappliedParent.getSec();
2519

    
2520
                                    // Determine the idInSource
2521
                                    String idInSourceParent = getIdInSource(misappliedParent);
2522

    
2523
                                    IZoologicalName parentSynZooName = getZoologicalName(misappliedParentName.getUuid(), zooHashMap);
2524
                                    String synParentGenus = parentSynZooName.getGenusOrUninomial();
2525
                                    String synParentInfragenericName = null;
2526
                                    String synParentSpecificEpithet = null;
2527

    
2528
                                    if (parentSynZooName.isInfraGeneric()){
2529
                                        synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2530
                                    }
2531
                                    if (parentSynZooName.isSpecies()){
2532
                                        synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2533
                                    }
2534

    
2535
                                    for (TaxonRelationship taxonRelationship: taxonRelListTaxon){
2536
                                        Taxon misappliedName = taxonRelationship.getFromTaxon();
2537
                                        IZoologicalName zooMisappliedName = getZoologicalName(misappliedName.getName().getUuid(), zooHashMap);
2538
                                        potentialCombination = createPotentialCombination(
2539
                                                idInSourceParent, parentSynZooName, zooMisappliedName,
2540
                                                synParentGenus,
2541
                                                synParentInfragenericName,
2542
                                                synParentSpecificEpithet, misappliedName, zooHashMap);
2543

    
2544
                                        taxon.addSynonym(potentialCombination, SynonymType.POTENTIAL_COMBINATION_OF());
2545
                                        inferredSynonyms.add(potentialCombination);
2546
                                        zooHashMap.put(potentialCombination.getName().getUuid(), potentialCombination.getName());
2547
                                         taxonNames.add(potentialCombination.getName().getNameCache());
2548
                                    }
2549
                                }
2550
                            }
2551

    
2552
                            if (!taxonNames.isEmpty()){
2553
                                List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2554
                                IZoologicalName name;
2555
                                if (!synNotInCDM.isEmpty()){
2556
                                    inferredSynonymsToBeRemoved.clear();
2557
                                    for (Synonym syn :inferredSynonyms){
2558
                                        try{
2559
                                            name = syn.getName();
2560
                                        }catch (ClassCastException e){
2561
                                            name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2562
                                        }
2563
                                        if (!synNotInCDM.contains(name.getNameCache())){
2564
                                            inferredSynonymsToBeRemoved.add(syn);
2565
                                        }
2566
                                     }
2567
                                    // Remove identified Synonyms from inferredSynonyms
2568
                                    for (Synonym synonym : inferredSynonymsToBeRemoved) {
2569
                                        inferredSynonyms.remove(synonym);
2570
                                    }
2571
                                }
2572
                            }
2573
                        }
2574
                    }else {
2575
                        logger.info("The synonym type is not defined.");
2576
                        return inferredSynonyms;
2577
                    }
2578
                }
2579
            }
2580
        }
2581

    
2582
        return inferredSynonyms;
2583
    }
2584

    
2585
    private Synonym createPotentialCombination(String idInSourceParent,
2586
            IZoologicalName parentSynZooName, 	IZoologicalName zooSynName, String synParentGenus,
2587
            String synParentInfragenericName, String synParentSpecificEpithet,
2588
            TaxonBase<?> syn, Map<UUID, IZoologicalName> zooHashMap) {
2589
        Synonym potentialCombination;
2590
        Reference sourceReference;
2591
        IZoologicalName inferredSynName;
2592
        HibernateProxyHelper.deproxy(syn);
2593

    
2594
        // Set sourceReference
2595
        sourceReference = syn.getSec();
2596
        if (sourceReference == null){
2597
            logger.warn("The synonym has no sec reference because it is a misapplied name! Take the sec reference of taxon");
2598
            //TODO:Remove
2599
            if (!parentSynZooName.getTaxa().isEmpty()){
2600
                TaxonBase<?> taxon = parentSynZooName.getTaxa().iterator().next();
2601

    
2602
                sourceReference = taxon.getSec();
2603
            }
2604
        }
2605
        String synTaxonSpecificEpithet = zooSynName.getSpecificEpithet();
2606

    
2607
        String synTaxonInfraSpecificName= null;
2608

    
2609
        if (parentSynZooName.isSpecies()){
2610
            synTaxonInfraSpecificName = zooSynName.getInfraSpecificEpithet();
2611
        }
2612

    
2613
        /*if (epithetName != null && !synonymsEpithet.contains(epithetName)){
2614
            synonymsEpithet.add(epithetName);
2615
        }*/
2616

    
2617
        //create potential combinations...
2618
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(syn.getName().getRank());
2619

    
2620
        inferredSynName.setGenusOrUninomial(synParentGenus);
2621
        if (zooSynName.isSpecies()){
2622
              inferredSynName.setSpecificEpithet(synTaxonSpecificEpithet);
2623
              if (parentSynZooName.isInfraGeneric()){
2624
                  inferredSynName.setInfraGenericEpithet(synParentInfragenericName);
2625
              }
2626
        }
2627
        if (zooSynName.isInfraSpecific()){
2628
            inferredSynName.setSpecificEpithet(synParentSpecificEpithet);
2629
            inferredSynName.setInfraSpecificEpithet(synTaxonInfraSpecificName);
2630
        }
2631
        if (parentSynZooName.isInfraGeneric()){
2632
            inferredSynName.setInfraGenericEpithet(synParentInfragenericName);
2633
        }
2634

    
2635
        potentialCombination = Synonym.NewInstance(inferredSynName, null);
2636

    
2637
        // Set the sourceReference
2638
        potentialCombination.setSec(sourceReference);
2639

    
2640

    
2641
        // Determine the idInSource
2642
        String idInSourceSyn= getIdInSource(syn);
2643

    
2644
        if (idInSourceParent != null && idInSourceSyn != null) {
2645
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2646
            inferredSynName.addSource(originalSource);
2647
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2648
            potentialCombination.addSource(originalSource);
2649
        }
2650

    
2651
        return potentialCombination;
2652
    }
2653

    
2654
    private Synonym createInferredGenus(Taxon taxon,
2655
            Map<UUID, IZoologicalName> zooHashMap, IZoologicalName taxonName,
2656
            String epithetOfTaxon, String genusOfTaxon,
2657
            List<String> taxonNames, IZoologicalName zooParentName,
2658
            TaxonBase syn) {
2659

    
2660
        Synonym inferredGenus;
2661
        TaxonName synName;
2662
        IZoologicalName inferredSynName;
2663
        synName =syn.getName();
2664
        HibernateProxyHelper.deproxy(syn);
2665

    
2666
        // Determine the idInSource
2667
        String idInSourceSyn = getIdInSource(syn);
2668
        String idInSourceTaxon = getIdInSource(taxon);
2669
        // Determine the sourceReference
2670
        Reference sourceReference = syn.getSec();
2671

    
2672
        //logger.warn(sourceReference.getTitleCache());
2673

    
2674
        synName = syn.getName();
2675
        IZoologicalName synZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2676
        String synSpeciesEpithetName = synZooName.getSpecificEpithet();
2677
         /* if (synonymsEpithet != null && !synonymsEpithet.contains(synSpeciesEpithetName)){
2678
            synonymsEpithet.add(synSpeciesEpithetName);
2679
        }*/
2680

    
2681
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2682
        //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...
2683

    
2684
        inferredSynName.setGenusOrUninomial(genusOfTaxon);
2685
        if (zooParentName.isInfraGeneric()){
2686
            inferredSynName.setInfraGenericEpithet(zooParentName.getInfraGenericEpithet());
2687
        }
2688

    
2689
        if (taxonName.isSpecies()){
2690
            inferredSynName.setSpecificEpithet(synSpeciesEpithetName);
2691
        }
2692
        if (taxonName.isInfraSpecific()){
2693
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2694
            inferredSynName.setInfraSpecificEpithet(synZooName.getInfraGenericEpithet());
2695
        }
2696

    
2697
        inferredGenus = Synonym.NewInstance(inferredSynName, null);
2698

    
2699
        // Set the sourceReference
2700
        inferredGenus.setSec(sourceReference);
2701

    
2702
        // Add the original source
2703
        if (idInSourceSyn != null && idInSourceTaxon != null) {
2704
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2705
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2706
            inferredGenus.addSource(originalSource);
2707

    
2708
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2709
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2710
            inferredSynName.addSource(originalSource);
2711
            originalSource = null;
2712

    
2713
        }else{
2714
            logger.error("There is an idInSource missing: " + idInSourceSyn + " of Synonym or " + idInSourceTaxon + " of Taxon");
2715
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2716
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2717
            inferredGenus.addSource(originalSource);
2718

    
2719
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2720
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2721
            inferredSynName.addSource(originalSource);
2722
            originalSource = null;
2723
        }
2724

    
2725
        taxon.addSynonym(inferredGenus, SynonymType.INFERRED_GENUS_OF());
2726

    
2727
        return inferredGenus;
2728
    }
2729

    
2730
    private Synonym createInferredEpithets(Taxon taxon,
2731
            Map<UUID, IZoologicalName> zooHashMap, IZoologicalName taxonName,
2732
            String epithetOfTaxon, String infragenericEpithetOfTaxon,
2733
            String infraspecificEpithetOfTaxon, List<String> taxonNames,
2734
            TaxonName parentName, TaxonBase<?> syn) {
2735

    
2736
        Synonym inferredEpithet;
2737
        TaxonName synName;
2738
        IZoologicalName inferredSynName;
2739
        HibernateProxyHelper.deproxy(syn);
2740

    
2741
        // Determine the idInSource
2742
        String idInSourceSyn = getIdInSource(syn);
2743
        String idInSourceTaxon =  getIdInSource(taxon);
2744
        // Determine the sourceReference
2745
        Reference sourceReference = syn.getSec();
2746

    
2747
        if (sourceReference == null){
2748
             logger.warn("The synonym has no sec reference because it is a misapplied name! Take the sec reference of taxon" + taxon.getSec());
2749
             sourceReference = taxon.getSec();
2750
        }
2751

    
2752
        synName = syn.getName();
2753
        IZoologicalName zooSynName = getZoologicalName(synName.getUuid(), zooHashMap);
2754
        String synGenusName = zooSynName.getGenusOrUninomial();
2755
        String synInfraGenericEpithet = null;
2756
        String synSpecificEpithet = null;
2757

    
2758
        if (zooSynName.getInfraGenericEpithet() != null){
2759
            synInfraGenericEpithet = zooSynName.getInfraGenericEpithet();
2760
        }
2761

    
2762
        if (zooSynName.isInfraSpecific()){
2763
            synSpecificEpithet = zooSynName.getSpecificEpithet();
2764
        }
2765

    
2766
           /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2767
            synonymsGenus.put(synGenusName, idInSource);
2768
        }*/
2769

    
2770
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2771

    
2772
        // DEBUG TODO: for subgenus or subspecies the infrageneric or infraspecific epithet should be used!!!
2773
        if (epithetOfTaxon == null && infragenericEpithetOfTaxon == null && infraspecificEpithetOfTaxon == null) {
2774
            logger.error("This specificEpithet is NULL" + taxon.getTitleCache());
2775
        }
2776
        inferredSynName.setGenusOrUninomial(synGenusName);
2777

    
2778
        if (parentName.isInfraGeneric()){
2779
            inferredSynName.setInfraGenericEpithet(synInfraGenericEpithet);
2780
        }
2781
        if (taxonName.isSpecies()){
2782
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2783
        }else if (taxonName.isInfraSpecific()){
2784
            inferredSynName.setSpecificEpithet(synSpecificEpithet);
2785
            inferredSynName.setInfraSpecificEpithet(infraspecificEpithetOfTaxon);
2786
        }
2787

    
2788
        inferredEpithet = Synonym.NewInstance(inferredSynName, null);
2789

    
2790
        // Set the sourceReference
2791
        inferredEpithet.setSec(sourceReference);
2792

    
2793
        /* Add the original source
2794
        if (idInSource != null) {
2795
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(idInSource, "InferredEpithetOf", syn.getSec(), null);
2796

    
2797
            // Add the citation
2798
            Reference citation = getCitation(syn);
2799
            if (citation != null) {
2800
                originalSource.setCitation(citation);
2801
                inferredEpithet.addSource(originalSource);
2802
            }
2803
        }*/
2804
        String taxonId = idInSourceTaxon+ "; " + idInSourceSyn;
2805

    
2806

    
2807
        IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2808
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2809

    
2810
        inferredEpithet.addSource(originalSource);
2811

    
2812
        originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2813
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2814

    
2815
        inferredSynName.addSource(originalSource);
2816

    
2817
        taxon.addSynonym(inferredEpithet, SynonymType.INFERRED_EPITHET_OF());
2818

    
2819
        return inferredEpithet;
2820
    }
2821

    
2822
    /**
2823
     * Returns an existing IZoologicalName or extends an internal hashmap if it does not exist.
2824
     * Very likely only useful for createInferredSynonyms().
2825
     * @param uuid
2826
     * @param zooHashMap
2827
     * @return
2828
     */
2829
    private IZoologicalName getZoologicalName(UUID uuid, Map <UUID, IZoologicalName> zooHashMap) {
2830
        IZoologicalName taxonName =nameDao.findZoologicalNameByUUID(uuid);
2831
        if (taxonName == null) {
2832
            taxonName = zooHashMap.get(uuid);
2833
        }
2834
        return taxonName;
2835
    }
2836

    
2837
    /**
2838
     * Returns the idInSource for a given Synonym.
2839
     * @param syn
2840
     */
2841
    private String getIdInSource(TaxonBase<?> taxonBase) {
2842
        String idInSource = null;
2843
        Set<IdentifiableSource> sources = taxonBase.getSources();
2844
        if (sources.size() == 1) {
2845
            IdentifiableSource source = sources.iterator().next();
2846
            if (source != null) {
2847
                idInSource  = source.getIdInSource();
2848
            }
2849
        } else if (sources.size() > 1) {
2850
            int count = 1;
2851
            idInSource = "";
2852
            for (IdentifiableSource source : sources) {
2853
                idInSource += source.getIdInSource();
2854
                if (count < sources.size()) {
2855
                    idInSource += "; ";
2856
                }
2857
                count++;
2858
            }
2859
        } else if (sources.size() == 0){
2860
            logger.warn("No idInSource for TaxonBase " + taxonBase.getUuid() + " - " + taxonBase.getTitleCache());
2861
        }
2862

    
2863
        return idInSource;
2864
    }
2865

    
2866
    /**
2867
     * Returns the citation for a given Synonym.
2868
     * @param syn
2869
     */
2870
    private Reference getCitation(Synonym syn) {
2871
        Reference citation = null;
2872
        Set<IdentifiableSource> sources = syn.getSources();
2873
        if (sources.size() == 1) {
2874
            IdentifiableSource source = sources.iterator().next();
2875
            if (source != null) {
2876
                citation = source.getCitation();
2877
            }
2878
        } else if (sources.size() > 1) {
2879
            logger.warn("This Synonym has more than one source: " + syn.getUuid() + " (" + syn.getTitleCache() +")");
2880
        }
2881

    
2882
        return citation;
2883
    }
2884

    
2885
    @Override
2886
    public List<Synonym>  createAllInferredSynonyms(Taxon taxon, Classification tree, boolean doWithMisappliedNames){
2887
        List <Synonym> inferredSynonyms = new ArrayList<>();
2888

    
2889
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.INFERRED_EPITHET_OF(), doWithMisappliedNames));
2890
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.INFERRED_GENUS_OF(), doWithMisappliedNames));
2891
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.POTENTIAL_COMBINATION_OF(), doWithMisappliedNames));
2892

    
2893
        return inferredSynonyms;
2894
    }
2895

    
2896
    @Override
2897
    public List<Classification> listClassifications(TaxonBase taxonBase, Integer limit, Integer start, List<String> propertyPaths) {
2898

    
2899
        // TODO quickly implemented, create according dao !!!!
2900
        Set<TaxonNode> nodes = new HashSet<>();
2901
        Set<Classification> classifications = new HashSet<>();
2902
        List<Classification> list = new ArrayList<>();
2903

    
2904
        if (taxonBase == null) {
2905
            return list;
2906
        }
2907

    
2908
        taxonBase = load(taxonBase.getUuid());
2909

    
2910
        if (taxonBase instanceof Taxon) {
2911
            nodes.addAll(((Taxon)taxonBase).getTaxonNodes());
2912
        } else {
2913
            Taxon taxon = ((Synonym)taxonBase).getAcceptedTaxon();
2914
            if (taxon != null){
2915
                nodes.addAll(taxon.getTaxonNodes());
2916
            }
2917
        }
2918
        for (TaxonNode node : nodes) {
2919
            classifications.add(node.getClassification());
2920
        }
2921
        list.addAll(classifications);
2922
        return list;
2923
    }
2924

    
2925
    @Override
2926
    @Transactional(readOnly = false)
2927
    public UpdateResult changeRelatedTaxonToSynonym(UUID fromTaxonUuid,
2928
            UUID toTaxonUuid,
2929
            TaxonRelationshipType oldRelationshipType,
2930
            SynonymType synonymType) throws DataChangeNoRollbackException {
2931
        UpdateResult result = new UpdateResult();
2932
        Taxon fromTaxon = (Taxon) dao.load(fromTaxonUuid);
2933
        Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
2934
        result = changeRelatedTaxonToSynonym(fromTaxon, toTaxon, oldRelationshipType, synonymType);
2935

    
2936
        result.addUpdatedObject(fromTaxon);
2937
        result.addUpdatedObject(toTaxon);
2938
        result.addUpdatedObject(result.getCdmEntity());
2939

    
2940
        return result;
2941
    }
2942

    
2943
    @Override
2944
    @Transactional(readOnly = false)
2945
    public UpdateResult changeRelatedTaxonToSynonym(Taxon fromTaxon, Taxon toTaxon, TaxonRelationshipType oldRelationshipType,
2946
            SynonymType synonymType) throws DataChangeNoRollbackException {
2947

    
2948
        UpdateResult result = new UpdateResult();
2949
        // Create new synonym using concept name
2950
        TaxonName synonymName = fromTaxon.getName();
2951

    
2952
        // Remove concept relation from taxon
2953
        toTaxon.removeTaxon(fromTaxon, oldRelationshipType);
2954

    
2955
        // Create a new synonym for the taxon
2956
        Synonym synonym;
2957
        if (synonymType != null
2958
                && synonymType.equals(SynonymType.HOMOTYPIC_SYNONYM_OF())){
2959
            synonym = Synonym.NewInstance(synonymName, fromTaxon.getSec());
2960
            toTaxon.addHomotypicSynonym(synonym);
2961
        } else{
2962
            synonym = toTaxon.addHeterotypicSynonymName(synonymName);
2963
        }
2964

    
2965
        this.saveOrUpdate(toTaxon);
2966
        //TODO: configurator and classification
2967
        TaxonDeletionConfigurator config = new TaxonDeletionConfigurator();
2968
        config.setDeleteNameIfPossible(false);
2969
        result.includeResult(this.deleteTaxon(fromTaxon.getUuid(), config, null));
2970
        result.setCdmEntity(synonym);
2971
        result.addUpdatedObject(toTaxon);
2972
        result.addUpdatedObject(synonym);
2973
        return result;
2974
    }
2975

    
2976
    @Override
2977
    public DeleteResult isDeletable(UUID taxonBaseUuid, DeleteConfiguratorBase config){
2978
        DeleteResult result = new DeleteResult();
2979
        TaxonBase<?> taxonBase = load(taxonBaseUuid);
2980
        Set<CdmBase> references = commonService.getReferencingObjectsForDeletion(taxonBase);
2981
        if (taxonBase instanceof Taxon){
2982
            TaxonDeletionConfigurator taxonConfig = (TaxonDeletionConfigurator) config;
2983
            List<String> propertyPaths = new ArrayList<>();
2984
            propertyPaths.add("taxonNodes");
2985
            Taxon taxon = (Taxon)load(taxonBaseUuid, propertyPaths);
2986

    
2987
            result = isDeletableForTaxon(references, taxonConfig );
2988
        }else{
2989
            SynonymDeletionConfigurator synonymConfig = (SynonymDeletionConfigurator) config;
2990
            result = isDeletableForSynonym(references, synonymConfig);
2991
        }
2992
        return result;
2993
    }
2994

    
2995
    private DeleteResult isDeletableForSynonym(Set<CdmBase> references, SynonymDeletionConfigurator config){
2996
        String message;
2997
        DeleteResult result = new DeleteResult();
2998
        for (CdmBase ref: references){
2999
            if (!(ref instanceof Taxon || ref instanceof TaxonName )){
3000
                message = "The Synonym can't be deleted as long as it is referenced by " + ref.getClass().getSimpleName() + " with id "+ ref.getId();
3001
                result.addException(new ReferencedObjectUndeletableException(message));
3002
                result.addRelatedObject(ref);
3003
                result.setAbort();
3004
            }
3005
        }
3006

    
3007
        return result;
3008
    }
3009

    
3010
    private DeleteResult isDeletableForTaxon(Set<CdmBase> references, TaxonDeletionConfigurator config){
3011
        String message = null;
3012
        DeleteResult result = new DeleteResult();
3013
        for (CdmBase ref: references){
3014
            if (!(ref instanceof TaxonName)){
3015
            	message = null;
3016
                if (!config.isDeleteSynonymRelations() && (ref instanceof Synonym)){
3017
                    message = "The taxon can't be deleted as long as it has synonyms.";
3018
                }
3019
                if (!config.isDeleteDescriptions() && (ref instanceof DescriptionBase)){
3020
                    message = "The taxon can't be deleted as long as it has factual data.";
3021
                }
3022

    
3023
                if (!config.isDeleteTaxonNodes() && (ref instanceof TaxonNode)){
3024

    
3025
                    message = "The taxon can't be deleted as long as it belongs to a taxon node.";
3026
                }
3027
                if (ref instanceof TaxonNode && config.getClassificationUuid() != null && !config.isDeleteInAllClassifications() && !((TaxonNode)ref).getClassification().getUuid().equals(config.getClassificationUuid())){
3028
                    message = "The taxon can't be deleted as long as it is used in more than one classification";
3029

    
3030
                }
3031
                if (!config.isDeleteTaxonRelationships() && (ref instanceof TaxonRelationship)){
3032
                    if (!config.isDeleteMisappliedNames() &&
3033
                            (((TaxonRelationship)ref).getType().isMisappliedName())){
3034
                        message = "The taxon can't be deleted as long as it has misapplied names or invalid designations.";
3035
                    } else{
3036
                        message = "The taxon can't be deleted as long as it belongs to taxon relationship.";
3037
                    }
3038
                }
3039
                if (ref instanceof PolytomousKeyNode){
3040
                    message = "The taxon can't be deleted as long as it is referenced by a polytomous key node.";
3041
                }
3042

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

    
3047
               /* //PolytomousKeyNode
3048
                if (referencingObject.isInstanceOf(PolytomousKeyNode.class)){
3049
                    String message = "Taxon" + taxon.getTitleCache() + " can't be deleted as it is used in polytomous key node";
3050
                    return message;
3051
                }*/
3052

    
3053
                //TaxonInteraction
3054
                if (ref.isInstanceOf(TaxonInteraction.class)){
3055
                    message = "Taxon can't be deleted as it is used in taxonInteraction#taxon2";
3056
                }
3057

    
3058
              //TaxonInteraction
3059
                if (ref.isInstanceOf(DeterminationEvent.class)){
3060
                    message = "Taxon can't be deleted as it is used in a determination event";
3061
                }
3062
            }
3063
            if (message != null){
3064
	            result.addException(new ReferencedObjectUndeletableException(message));
3065
	            result.addRelatedObject(ref);
3066
	            result.setAbort();
3067
            }
3068
        }
3069

    
3070
        return result;
3071
    }
3072

    
3073
    @Override
3074
    public IncludedTaxaDTO listIncludedTaxa(UUID taxonUuid, IncludedTaxonConfiguration config) {
3075
        IncludedTaxaDTO result = new IncludedTaxaDTO(taxonUuid);
3076

    
3077
        //preliminary implementation
3078

    
3079
        Set<Taxon> taxa = new HashSet<>();
3080
        TaxonBase<?> taxonBase = find(taxonUuid);
3081
        if (taxonBase == null){
3082
            return new IncludedTaxaDTO();
3083
        }else if (taxonBase.isInstanceOf(Taxon.class)){
3084
            Taxon taxon = CdmBase.deproxy(taxonBase, Taxon.class);
3085
            taxa.add(taxon);
3086
        }else if (taxonBase.isInstanceOf(Synonym.class)){
3087
            //TODO partial synonyms ??
3088
            //TODO synonyms in general
3089
            Synonym syn = CdmBase.deproxy(taxonBase, Synonym.class);
3090
            taxa.add(syn.getAcceptedTaxon());
3091
        }else{
3092
            throw new IllegalArgumentException("Unhandled class " + taxonBase.getClass().getSimpleName());
3093
        }
3094

    
3095
        Set<Taxon> related = makeRelatedIncluded(taxa, result, config);
3096
        int i = 0;
3097
        while((! related.isEmpty()) && i++ < 100){  //to avoid
3098
             related = makeRelatedIncluded(related, result, config);
3099
        }
3100

    
3101
        return result;
3102
    }
3103

    
3104
    /**
3105
     * @param uncheckedTaxa
3106
     * @param existingTaxa
3107
     * @param config
3108
     *
3109
     * Computes all children and conceptually congruent and included taxa and adds them to the existingTaxa
3110
     * data structure.
3111
     * @return the set of conceptually related taxa for further use
3112
     */
3113
    private Set<Taxon> makeRelatedIncluded(Set<Taxon> uncheckedTaxa, IncludedTaxaDTO existingTaxa, IncludedTaxonConfiguration config) {
3114

    
3115
        //children
3116
        Set<TaxonNode> taxonNodes = new HashSet<>();
3117
        for (Taxon taxon: uncheckedTaxa){
3118
            taxonNodes.addAll(taxon.getTaxonNodes());
3119
        }
3120

    
3121
        Set<Taxon> children = new HashSet<>();
3122
        if (! config.onlyCongruent){
3123
            for (TaxonNode node: taxonNodes){
3124
                List<TaxonNode> childNodes = nodeService.loadChildNodesOfTaxonNode(node, null, true, config.includeUnpublished, null);
3125
                for (TaxonNode child : childNodes){
3126
                    children.add(child.getTaxon());
3127
                }
3128
            }
3129
            children.remove(null);  // just to be on the save side
3130
        }
3131

    
3132
        Iterator<Taxon> it = children.iterator();
3133
        while(it.hasNext()){
3134
            UUID uuid = it.next().getUuid();
3135
            if (existingTaxa.contains(uuid)){
3136
                it.remove();
3137
            }else{
3138
                existingTaxa.addIncludedTaxon(uuid, new ArrayList<>(), false);
3139
            }
3140
        }
3141

    
3142
        //concept relations
3143
        Set<Taxon> uncheckedAndChildren = new HashSet<>(uncheckedTaxa);
3144
        uncheckedAndChildren.addAll(children);
3145

    
3146
        Set<Taxon> relatedTaxa = makeConceptIncludedTaxa(uncheckedAndChildren, existingTaxa, config);
3147

    
3148

    
3149
        Set<Taxon> result = new HashSet<>(relatedTaxa);
3150
        return result;
3151
    }
3152

    
3153
    /**
3154
     * Computes all conceptually congruent or included taxa and adds them to the existingTaxa data structure.
3155
     * @return the set of these computed taxa
3156
     */
3157
    private Set<Taxon> makeConceptIncludedTaxa(Set<Taxon> unchecked, IncludedTaxaDTO existingTaxa, IncludedTaxonConfiguration config) {
3158
        Set<Taxon> result = new HashSet<>();
3159

    
3160
        for (Taxon taxon : unchecked){
3161
            Set<TaxonRelationship> fromRelations = taxon.getRelationsFromThisTaxon();
3162
            Set<TaxonRelationship> toRelations = taxon.getRelationsToThisTaxon();
3163

    
3164
            for (TaxonRelationship fromRel : fromRelations){
3165
                if (config.includeDoubtful == false && fromRel.isDoubtful()){
3166
                    continue;
3167
                }
3168
                TaxonRelationshipType fromRelType = fromRel.getType();
3169
                if (fromRelType.equals(TaxonRelationshipType.CONGRUENT_TO()) ||
3170
                        !config.onlyCongruent && (
3171
                            fromRelType.equals(TaxonRelationshipType.INCLUDES()) ||
3172
                            fromRelType.equals(TaxonRelationshipType.CONGRUENT_OR_INCLUDES())
3173
                        )
3174
                    ){
3175
                    result.add(fromRel.getToTaxon());
3176
                }
3177
            }
3178

    
3179
            for (TaxonRelationship toRel : toRelations){
3180
                if (config.includeDoubtful == false && toRel.isDoubtful()){
3181
                    continue;
3182
                }
3183
                TaxonRelationshipType fromRelType = toRel.getType();
3184
                if (fromRelType.equals(TaxonRelationshipType.CONGRUENT_TO()) ||
3185
                        !config.includeDoubtful && fromRelType.equals(TaxonRelationshipType.TAXONOMICALLY_INCLUDED_IN())){
3186
                    result.add(toRel.getFromTaxon());
3187
                }
3188
            }
3189
        }
3190

    
3191
        Iterator<Taxon> it = result.iterator();
3192
        while(it.hasNext()){
3193
            UUID uuid = it.next().getUuid();
3194
            if (existingTaxa.contains(uuid)){
3195
                it.remove();
3196
            }else{
3197
                existingTaxa.addIncludedTaxon(uuid, new ArrayList<>(), false);
3198
            }
3199
        }
3200
        return result;
3201
    }
3202

    
3203
    @Override
3204
    public List<TaxonBase> findTaxaByName(MatchingTaxonConfigurator config){
3205
        @SuppressWarnings("rawtypes")
3206
        List<TaxonBase> taxonList = dao.getTaxaByName(true, config.isIncludeSynonyms(), false, false, false,
3207
                config.getTaxonNameTitle(), null, null, MatchMode.EXACT, null, config.isIncludeSynonyms(), null, 0, 0, config.getPropertyPath());
3208
        return taxonList;
3209
    }
3210

    
3211
	@Override
3212
	@Transactional(readOnly = true)
3213
	public <S extends TaxonBase> Pager<IdentifiedEntityDTO<S>> findByIdentifier(
3214
			Class<S> clazz, String identifier, DefinedTerm identifierType, TaxonNode subtreeFilter,
3215
			MatchMode matchmode, boolean includeEntity, Integer pageSize,
3216
			Integer pageNumber,	List<String> propertyPaths) {
3217
		if (subtreeFilter == null){
3218
			return findByIdentifier(clazz, identifier, identifierType, matchmode, includeEntity, pageSize, pageNumber, propertyPaths);
3219
		}
3220

    
3221
		long numberOfResults = dao.countByIdentifier(clazz, identifier, identifierType, subtreeFilter, matchmode);
3222
        List<Object[]> daoResults = new ArrayList<>();
3223
        if(numberOfResults > 0) { // no point checking again
3224
        	daoResults = dao.findByIdentifier(clazz, identifier, identifierType, subtreeFilter,
3225
    				matchmode, includeEntity, pageSize, pageNumber, propertyPaths);
3226
        }
3227

    
3228
        List<IdentifiedEntityDTO<S>> result = new ArrayList<>();
3229
        for (Object[] daoObj : daoResults){
3230
        	if (includeEntity){
3231
        		result.add(new IdentifiedEntityDTO<>((DefinedTerm)daoObj[0], (String)daoObj[1], (S)daoObj[2]));
3232
        	}else{
3233
        		result.add(new IdentifiedEntityDTO<>((DefinedTerm)daoObj[0], (String)daoObj[1], (UUID)daoObj[2], (String)daoObj[3], null));
3234
        	}
3235
        }
3236
		return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, result);
3237
	}
3238

    
3239
	@Override
3240
    @Transactional(readOnly = true)
3241
    public <S extends TaxonBase> Pager<MarkedEntityDTO<S>> findByMarker(
3242
            Class<S> clazz, MarkerType markerType, Boolean markerValue,
3243
            TaxonNode subtreeFilter, boolean includeEntity, TaxonTitleType titleType,
3244
            Integer pageSize, Integer pageNumber, List<String> propertyPaths) {
3245
        if (subtreeFilter == null){
3246
            return super.findByMarker (clazz, markerType, markerValue, includeEntity, pageSize, pageNumber, propertyPaths);
3247
        }
3248

    
3249
        Long numberOfResults = dao.countByMarker(clazz, markerType, markerValue, subtreeFilter);
3250
        List<Object[]> daoResults = new ArrayList<>();
3251
        if(numberOfResults > 0) { // no point checking again
3252
            daoResults = dao.findByMarker(clazz, markerType, markerValue, subtreeFilter,
3253
                    includeEntity, titleType, pageSize, pageNumber, propertyPaths);
3254
        }
3255

    
3256
        List<MarkedEntityDTO<S>> result = new ArrayList<>();
3257
        for (Object[] daoObj : daoResults){
3258
            if (includeEntity){
3259
                result.add(new MarkedEntityDTO<S>((MarkerType)daoObj[0], (Boolean)daoObj[1], (S)daoObj[2]));
3260
            }else{
3261
                result.add(new MarkedEntityDTO<S>((MarkerType)daoObj[0], (Boolean)daoObj[1], (UUID)daoObj[2], (String)daoObj[3]));
3262
            }
3263
        }
3264
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, result);
3265
    }
3266

    
3267
    @Override
3268
	@Transactional(readOnly = false)
3269
	public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym, UUID newTaxonUUID, boolean moveHomotypicGroup,
3270
            SynonymType newSynonymType, Reference newSecundum, String newSecundumDetail,
3271
            boolean keepSecundumIfUndefined) throws HomotypicalGroupChangeException {
3272

    
3273
	    UpdateResult result = new UpdateResult();
3274
		Taxon newTaxon = CdmBase.deproxy(dao.load(newTaxonUUID),Taxon.class);
3275
		result = moveSynonymToAnotherTaxon(oldSynonym, newTaxon, moveHomotypicGroup, newSynonymType,
3276
		        newSecundum, newSecundumDetail, keepSecundumIfUndefined);
3277

    
3278
		return result;
3279
	}
3280

    
3281
	@Override
3282
	public UpdateResult moveFactualDateToAnotherTaxon(UUID fromTaxonUuid, UUID toTaxonUuid){
3283
		UpdateResult result = new UpdateResult();
3284

    
3285
		Taxon fromTaxon = (Taxon)dao.load(fromTaxonUuid);
3286
		Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
3287
    	for(TaxonDescription description : fromTaxon.getDescriptions()){
3288
              //reload to avoid session conflicts
3289
              description = HibernateProxyHelper.deproxy(description, TaxonDescription.class);
3290

    
3291
              String moveMessage = String.format("Description moved from %s", fromTaxon);
3292
              if(description.isProtectedTitleCache()){
3293
                  String separator = "";
3294
                  if(!StringUtils.isBlank(description.getTitleCache())){
3295
                      separator = " - ";
3296
                  }
3297
                  description.setTitleCache(description.getTitleCache() + separator + moveMessage, true);
3298
              }
3299
              Annotation annotation = Annotation.NewInstance(moveMessage, Language.getDefaultLanguage());
3300
              annotation.setAnnotationType(AnnotationType.TECHNICAL());
3301
              description.addAnnotation(annotation);
3302
              toTaxon.addDescription(description);
3303
              dao.saveOrUpdate(toTaxon);
3304
              dao.saveOrUpdate(fromTaxon);
3305
              result.addUpdatedObject(toTaxon);
3306
              result.addUpdatedObject(fromTaxon);
3307

    
3308
        }
3309

    
3310
    	return result;
3311
	}
3312

    
3313
	@Override
3314
	@Transactional(readOnly = false)
3315
	public UpdateResult swapSynonymAndAcceptedTaxon(UUID synonymUUid,
3316
			UUID acceptedTaxonUuid, boolean setNameInSource) {
3317
		TaxonBase<?> base = this.load(synonymUUid);
3318
		Synonym syn = HibernateProxyHelper.deproxy(base, Synonym.class);
3319
		base = this.load(acceptedTaxonUuid);
3320
		Taxon taxon = HibernateProxyHelper.deproxy(base, Taxon.class);
3321

    
3322
		return this.swapSynonymAndAcceptedTaxon(syn, taxon, setNameInSource);
3323
	}
3324

    
3325
    @Override
3326
    public TaxonRelationshipsDTO listTaxonRelationships(UUID taxonUuid, Set<TaxonRelationshipType> directTypes,
3327
            Set<TaxonRelationshipType> inversTypes,
3328
            Direction direction, boolean groupMisapplications,
3329
            boolean includeUnpublished,
3330
            Integer pageSize, Integer pageNumber) {
3331
        TaxonBase<?> taxonBase = dao.load(taxonUuid);
3332
        if (taxonBase == null || !taxonBase.isInstanceOf(TaxonBase.class)){
3333
            //TODO handle
3334
            throw new RuntimeException("Taxon for uuid " + taxonUuid + " not found");
3335
        }else{
3336
            Taxon taxon = CdmBase.deproxy(taxonBase, Taxon.class);
3337
            boolean doDirect = (direction == null || direction == Direction.relatedTo);
3338
            boolean doInvers = (direction == null || direction == Direction.relatedFrom);
3339

    
3340
            TaxonRelationshipsDTO dto = new TaxonRelationshipsDTO();
3341

    
3342
            //TODO paging is difficult because misapplication string is an attribute
3343
            //of toplevel dto
3344
//        long numberOfResults = dao.countTaxonRelationships(taxon, type, includeUnpublished, TaxonRelationship.Direction.relatedFrom);
3345
//        List<TaxonRelationshipsDTO> results = new ArrayList<>();
3346
//        if(numberOfResults > 0) { // no point checking again
3347
//            results = dao.getTaxonRelationships(taxon, type, includeUnpublished, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedFrom);
3348
//        }
3349
//
3350
//        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);;
3351

    
3352
            //TODO languages
3353
            List<Language> languages = null;
3354
            if (doDirect){
3355
                direction = Direction.relatedTo;
3356
                //TODO order hints, property path
3357
                List<TaxonRelationship> relations = dao.getTaxonRelationships(taxon, directTypes, includeUnpublished, pageSize, pageNumber, null, null, direction.invers());
3358
                for (TaxonRelationship relation : relations){
3359
                    dto.addRelation(relation, direction, languages);
3360
                }
3361
            }
3362
            if (doInvers){
3363
                direction = Direction.relatedFrom;
3364
                //TODO order hints, property path
3365
                List<TaxonRelationship> relations = dao.getTaxonRelationships(taxon, inversTypes, includeUnpublished, pageSize, pageNumber, null, null, direction.invers());
3366
                for (TaxonRelationship relation : relations){
3367
                    dto.addRelation(relation, direction, languages);
3368
                }
3369
            }
3370
            if (groupMisapplications){
3371
                //TODO
3372
                dto.createMisapplicationString();
3373
            }
3374
            return dto;
3375
        }
3376
    }
3377
}
(92-92/100)