Project

General

Profile

Download (159 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.EnumSet;
15
import java.util.HashMap;
16
import java.util.HashSet;
17
import java.util.Iterator;
18
import java.util.List;
19
import java.util.Map;
20
import java.util.Set;
21
import java.util.UUID;
22

    
23
import javax.persistence.EntityNotFoundException;
24

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

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

    
132

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

    
143
    private static final Logger logger = Logger.getLogger(TaxonServiceImpl.class);
144

    
145
    public static final String POTENTIAL_COMBINATION_NAMESPACE = "Potential combination";
146

    
147
    public static final String INFERRED_EPITHET_NAMESPACE = "Inferred epithet";
148

    
149
    public static final String INFERRED_GENUS_NAMESPACE = "Inferred genus";
150

    
151
    @Autowired
152
    private ITaxonNodeDao taxonNodeDao;
153

    
154
    @Autowired
155
    private ITaxonNameDao nameDao;
156

    
157
    @Autowired
158
    private INameService nameService;
159

    
160
    @Autowired
161
    private IOccurrenceService occurrenceService;
162

    
163
    @Autowired
164
    private ITaxonNodeService nodeService;
165

    
166
    @Autowired
167
    private IDescriptionService descriptionService;
168
//
169
//    @Autowired
170
//    private IOrderedTermVocabularyDao orderedVocabularyDao;
171

    
172
    @Autowired
173
    private IOccurrenceDao occurrenceDao;
174

    
175
    @Autowired
176
    private IClassificationDao classificationDao;
177

    
178
    @Autowired
179
    private AbstractBeanInitializer beanInitializer;
180

    
181
    @Autowired
182
    private ILuceneIndexToolProvider luceneIndexToolProvider;
183

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

    
189
// ****************************** METHODS ********************************/
190

    
191

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

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

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

    
213
        synonymName.removeTaxonBase(synonym);
214

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

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

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

    
241

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

    
249
            }else if(toTaxon == acceptedTaxon){
250
               fromTaxon.addTaxonRelation(newTaxon, taxonRelationship.getType(),
251
                        taxonRelationship.getCitation(), taxonRelationship.getCitationMicroReference());
252
               saveOrUpdate(fromTaxon);
253

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

    
259
            fromTaxon.removeTaxonRelation(taxonRelationship);
260
            toTaxon.removeTaxonRelation(taxonRelationship);
261
            taxonRelationship.setToTaxon(null);
262
            taxonRelationship.setFromTaxon(null);
263
        }
264

    
265

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

    
294
        }
295
        Synonym newSynonym = (Synonym)synonym.clone();
296
        newSynonym.setName(taxonName);
297
        newSynonym.setSec(acceptedTaxon.getSec());
298
        if (sameHomotypicGroup){
299
            newTaxon.addSynonym(newSynonym, SynonymType.HOMOTYPIC_SYNONYM_OF());
300
        }else{
301
            newTaxon.addSynonym(newSynonym, SynonymType.HETEROTYPIC_SYNONYM_OF());
302
        }
303

    
304
        saveOrUpdate(newSynonym);
305
        saveOrUpdate(newTaxon);
306
        TaxonDeletionConfigurator conf = new TaxonDeletionConfigurator();
307
        conf.setDeleteNameIfPossible(false);
308
        SynonymDeletionConfigurator confSyn = new SynonymDeletionConfigurator();
309
        confSyn.setDeleteNameIfPossible(false);
310
        result.setCdmEntity(newTaxon);
311

    
312
        DeleteResult deleteResult = deleteTaxon(acceptedTaxon.getUuid(), conf, null);
313
        if (synonym.isPersited()){
314
            deleteResult.includeResult(deleteSynonym(synonym, confSyn));
315
        }
316
        result.includeResult(deleteResult);
317
		return result;
318

    
319

    
320
    }
321

    
322

    
323
    @Override
324
    @Transactional(readOnly = false)
325
    public UpdateResult changeSynonymToAcceptedTaxon(Synonym synonym, Taxon acceptedTaxon, boolean deleteSynonym) {
326
        UpdateResult result = new UpdateResult();
327
        TaxonName acceptedName = acceptedTaxon.getName();
328
        TaxonName synonymName = synonym.getName();
329
        HomotypicalGroup synonymHomotypicGroup = synonymName.getHomotypicalGroup();
330

    
331
        //check synonym is not homotypic
332
        if (acceptedName.getHomotypicalGroup().equals(synonymHomotypicGroup)){
333
            String message = "The accepted taxon and the synonym are part of the same homotypical group and therefore can not be both accepted.";
334
            result.addException(new HomotypicalGroupChangeException(message));
335
            result.setAbort();
336
            return result;
337
        }
338

    
339
        Taxon newAcceptedTaxon = Taxon.NewInstance(synonymName, acceptedTaxon.getSec());
340
        dao.save(newAcceptedTaxon);
341
        result.setCdmEntity(newAcceptedTaxon);
342
        SynonymType relTypeForGroup = SynonymType.HOMOTYPIC_SYNONYM_OF();
343
        List<Synonym> heteroSynonyms = acceptedTaxon.getSynonymsInGroup(synonymHomotypicGroup);
344

    
345
        for (Synonym heteroSynonym : heteroSynonyms){
346
            if (synonym.equals(heteroSynonym)){
347
                acceptedTaxon.removeSynonym(heteroSynonym, false);
348
            }else{
349
                //move synonyms in same homotypic group to new accepted taxon
350
                newAcceptedTaxon.addSynonym(heteroSynonym, relTypeForGroup);
351
            }
352
        }
353
        dao.saveOrUpdate(acceptedTaxon);
354
        result.addUpdatedObject(acceptedTaxon);
355
        if (deleteSynonym){
356

    
357
            try {
358
                this.dao.flush();
359
                SynonymDeletionConfigurator config = new SynonymDeletionConfigurator();
360
                config.setDeleteNameIfPossible(false);
361
                this.deleteSynonym(synonym, config);
362

    
363
            } catch (Exception e) {
364
                result.addException(e);
365
            }
366
        }
367

    
368
        return result;
369
    }
370

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

    
391

    
392

    
393

    
394
    @Override
395
    @Transactional(readOnly = false)
396
    public UpdateResult changeSynonymToRelatedTaxon(UUID synonymUuid,
397
            UUID toTaxonUuid,
398
            TaxonRelationshipType taxonRelationshipType,
399
            Reference citation,
400
            String microcitation){
401

    
402
        UpdateResult result = new UpdateResult();
403
        Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
404
        Synonym synonym = (Synonym) dao.load(synonymUuid);
405
        result = changeSynonymToRelatedTaxon(synonym, toTaxon, taxonRelationshipType, citation, microcitation);
406
        Taxon relatedTaxon = (Taxon)result.getCdmEntity();
407
//        result.setCdmEntity(relatedTaxon);
408
        result.addUpdatedObject(relatedTaxon);
409
        result.addUpdatedObject(toTaxon);
410
        return result;
411
    }
412

    
413
    @Override
414
    @Transactional(readOnly = false)
415
    public UpdateResult changeSynonymToRelatedTaxon(Synonym synonym, Taxon toTaxon, TaxonRelationshipType taxonRelationshipType, Reference citation, String microcitation){
416
        // Get name from synonym
417
        if (synonym == null){
418
            return null;
419
        }
420

    
421
        UpdateResult result = new UpdateResult();
422

    
423
        TaxonName synonymName = synonym.getName();
424

    
425
      /*  // remove synonym from taxon
426
        toTaxon.removeSynonym(synonym);
427
*/
428
        // Create a taxon with synonym name
429
        Taxon fromTaxon = Taxon.NewInstance(synonymName, null);
430
        save(fromTaxon);
431
        fromTaxon.setAppendedPhrase(synonym.getAppendedPhrase());
432

    
433
        // Add taxon relation
434
        fromTaxon.addTaxonRelation(toTaxon, taxonRelationshipType, citation, microcitation);
435
        result.setCdmEntity(fromTaxon);
436
        // since we are swapping names, we have to detach the name from the synonym completely.
437
        // Otherwise the synonym will still be in the list of typified names.
438
       // synonym.getName().removeTaxonBase(synonym);
439
        result.includeResult(this.deleteSynonym(synonym, null));
440

    
441
        return result;
442
    }
443

    
444
    @Transactional(readOnly = false)
445
    @Override
446
    public void changeHomotypicalGroupOfSynonym(Synonym synonym, HomotypicalGroup newHomotypicalGroup,
447
            Taxon targetTaxon, boolean setBasionymRelationIfApplicable){
448
        // Get synonym name
449
        TaxonName synonymName = synonym.getName();
450
        HomotypicalGroup oldHomotypicalGroup = synonymName.getHomotypicalGroup();
451

    
452
        // Switch groups
453
        oldHomotypicalGroup.removeTypifiedName(synonymName, false);
454
        newHomotypicalGroup.addTypifiedName(synonymName);
455

    
456
        //remove existing basionym relationships
457
        synonymName.removeBasionyms();
458

    
459
        //add basionym relationship
460
        if (setBasionymRelationIfApplicable){
461
            Set<TaxonName> basionyms = newHomotypicalGroup.getBasionyms();
462
            for (TaxonName basionym : basionyms){
463
                synonymName.addBasionym(basionym);
464
            }
465
        }
466

    
467
        //set synonym relationship correctly
468
        Taxon acceptedTaxon = synonym.getAcceptedTaxon();
469

    
470
        boolean hasNewTargetTaxon = targetTaxon != null && !targetTaxon.equals(acceptedTaxon);
471
        if (acceptedTaxon != null){
472

    
473
            HomotypicalGroup acceptedGroup = acceptedTaxon.getHomotypicGroup();
474
            boolean isHomotypicToTaxon = acceptedGroup.equals(newHomotypicalGroup);
475
            SynonymType newRelationType = isHomotypicToTaxon? SynonymType.HOMOTYPIC_SYNONYM_OF() : SynonymType.HETEROTYPIC_SYNONYM_OF();
476
            synonym.setType(newRelationType);
477

    
478
            if (hasNewTargetTaxon){
479
                acceptedTaxon.removeSynonym(synonym, false);
480
            }
481
        }
482
        if (hasNewTargetTaxon ){
483
            @SuppressWarnings("null")
484
            HomotypicalGroup acceptedGroup = targetTaxon.getHomotypicGroup();
485
            boolean isHomotypicToTaxon = acceptedGroup.equals(newHomotypicalGroup);
486
            SynonymType relType = isHomotypicToTaxon? SynonymType.HOMOTYPIC_SYNONYM_OF() : SynonymType.HETEROTYPIC_SYNONYM_OF();
487
            targetTaxon.addSynonym(synonym, relType);
488
        }
489

    
490
    }
491

    
492
    @Override
493
    @Transactional(readOnly = false)
494
    public UpdateResult updateCaches(Class<? extends TaxonBase> clazz, Integer stepSize, IIdentifiableEntityCacheStrategy<TaxonBase> cacheStrategy, IProgressMonitor monitor) {
495
        if (clazz == null){
496
            clazz = TaxonBase.class;
497
        }
498
        return super.updateCachesImpl(clazz, stepSize, cacheStrategy, monitor);
499
    }
500

    
501
    @Override
502
    @Autowired
503
    protected void setDao(ITaxonDao dao) {
504
        this.dao = dao;
505
    }
506

    
507

    
508
    @Override
509
    public <T extends TaxonBase> Pager<T> findTaxaByName(Class<T> clazz, String uninomial,	String infragenericEpithet, String specificEpithet,
510
            String infraspecificEpithet, String authorshipCache, Rank rank, Integer pageSize,Integer pageNumber, List<String> propertyPaths) {
511
        long numberOfResults = dao.countTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, authorshipCache, rank);
512

    
513
        List<T> results = new ArrayList<>();
514
        if(numberOfResults > 0) { // no point checking again
515
            results = dao.findTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, authorshipCache, rank,
516
                    pageSize, pageNumber, propertyPaths);
517
        }
518

    
519
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
520
    }
521

    
522
    @Override
523
    public <T extends TaxonBase> List<T> listTaxaByName(Class<T> clazz, String uninomial, String infragenericEpithet, String specificEpithet,
524
            String infraspecificEpithet, String authorshipCache, Rank rank, Integer pageSize,Integer pageNumber, List<String> propertyPaths) {
525

    
526
        return findTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infragenericEpithet, authorshipCache, rank,
527
                pageSize, pageNumber, propertyPaths).getRecords();
528
    }
529

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

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

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

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

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

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

    
566
    @Override
567
    public Pager<TaxonRelationship> pageFromTaxonRelationships(Taxon taxon, TaxonRelationshipType type,
568
            boolean includeUnpublished, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
569
        long numberOfResults = dao.countTaxonRelationships(taxon, type, includeUnpublished, TaxonRelationship.Direction.relatedFrom);
570

    
571
        List<TaxonRelationship> results = new ArrayList<>();
572
        if(numberOfResults > 0) { // no point checking again
573
            results = dao.getTaxonRelationships(taxon, type, includeUnpublished, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedFrom);
574
        }
575
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
576
    }
577

    
578
    @Override
579
    public List<TaxonRelationship> listTaxonRelationships(Set<TaxonRelationshipType> types,
580
            Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
581

    
582
        Long numberOfResults = dao.countTaxonRelationships(types);
583
        List<TaxonRelationship> results = new ArrayList<>();
584
        if(numberOfResults > 0) {
585
            results = dao.getTaxonRelationships(types, pageSize, pageNumber, orderHints, propertyPaths);
586
        }
587
        return results;
588
    }
589

    
590
    @Override
591
    public <S extends TaxonBase> Pager<S> page(Class<S> clazz, List<Restriction<?>> restrictions, Integer pageSize,
592
            Integer pageIndex, List<OrderHint> orderHints, List<String> propertyPaths) {
593
        return page(clazz, restrictions, pageSize, pageIndex, orderHints, propertyPaths, INCLUDE_UNPUBLISHED);
594
    }
595

    
596
    @Override
597
    public <S extends TaxonBase> Pager<S> page(Class<S> clazz, List<Restriction<?>> restrictions, Integer pageSize,
598
            Integer pageIndex, List<OrderHint> orderHints, List<String> propertyPaths, boolean includeUnpublished) {
599

    
600
        List<S> records;
601
        long resultSize = dao.count(clazz, restrictions);
602
        if(AbstractPagerImpl.hasResultsInRange(resultSize, pageIndex, pageSize)){
603
            records = dao.list(clazz, restrictions, pageSize, pageIndex, orderHints, propertyPaths, includeUnpublished);
604
        } else {
605
            records = new ArrayList<>();
606
        }
607
        Pager<S> pager = new DefaultPagerImpl<>(pageIndex, resultSize, pageSize, records);
608
        return pager;
609
    }
610

    
611
    @Override
612
    public Taxon findAcceptedTaxonFor(UUID synonymUuid, UUID classificationUuid,
613
            boolean includeUnpublished, List<String> propertyPaths) throws UnpublishedException{
614

    
615
        Synonym synonym = null;
616

    
617
        try {
618
            synonym = (Synonym) dao.load(synonymUuid);
619
            checkPublished(synonym, includeUnpublished, "Synoym is unpublished.");
620
        } catch (ClassCastException e){
621
            throw new EntityNotFoundException("The TaxonBase entity referenced by " + synonymUuid + " is not a Synonmy");
622
        } catch (NullPointerException e){
623
            throw new EntityNotFoundException("No TaxonBase entity found for " + synonymUuid);
624
        }
625

    
626
        Classification classificationFilter = null;
627
        if(classificationUuid != null){
628
            try {
629
                classificationFilter = classificationDao.load(classificationUuid);
630
            } catch (NullPointerException e){
631
                //TODO not sure, why an NPE should be thrown in the above load method
632
                throw new EntityNotFoundException("No Classification entity found for " + classificationUuid);
633
            }
634
            if(classificationFilter == null){
635
                throw new EntityNotFoundException("No Classification entity found for " + classificationUuid);
636
            }
637
        }
638

    
639
        long count = dao.countAcceptedTaxonFor(synonym, classificationFilter) ;
640
        if(count > 0){
641
            Taxon result = dao.acceptedTaxonFor(synonym, classificationFilter, propertyPaths);
642
            checkPublished(result, includeUnpublished, "Accepted taxon unpublished");
643
            return result;
644
        }else{
645
            return null;
646
        }
647
    }
648

    
649
    @Override
650
    public Set<Taxon> listRelatedTaxa(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, Integer maxDepth,
651
            boolean includeUnpublished, Integer limit, Integer start, List<String> propertyPaths) {
652

    
653
        Set<Taxon> relatedTaxa = collectRelatedTaxa(taxon, includeRelationships, new HashSet<>(), includeUnpublished, maxDepth);
654
        relatedTaxa.remove(taxon);
655
        beanInitializer.initializeAll(relatedTaxa, propertyPaths);
656
        return relatedTaxa;
657
    }
658

    
659

    
660
    /**
661
     * Recursively collect related taxa for the given <code>taxon</code> . The returned list will also include the
662
     *  <code>taxon</code> supplied as parameter.
663
     *
664
     * @param taxon
665
     * @param includeRelationships
666
     * @param taxa
667
     * @param maxDepth can be <code>null</code> for infinite depth
668
     * @return
669
     */
670
    private Set<Taxon> collectRelatedTaxa(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, Set<Taxon> taxa,
671
            boolean includeUnpublished, Integer maxDepth) {
672

    
673
        if(taxa.isEmpty()) {
674
            taxa.add(taxon);
675
        }
676

    
677
        if(includeRelationships.isEmpty()){
678
            return taxa;
679
        }
680

    
681
        if(maxDepth != null) {
682
            maxDepth--;
683
        }
684
        if(logger.isDebugEnabled()){
685
            logger.debug("collecting related taxa for " + taxon + " with maxDepth=" + maxDepth);
686
        }
687
        List<TaxonRelationship> taxonRelationships = dao.getTaxonRelationships(taxon,
688
                (Set<TaxonRelationshipType>)null, includeUnpublished, null, null, null, null, null);
689
        for (TaxonRelationship taxRel : taxonRelationships) {
690

    
691
            // skip invalid data
692
            if (taxRel.getToTaxon() == null || taxRel.getFromTaxon() == null || taxRel.getType() == null) {
693
                continue;
694
            }
695
            // filter by includeRelationships
696
            for (TaxonRelationshipEdge relationshipEdgeFilter : includeRelationships) {
697
                if ( relationshipEdgeFilter.getRelationshipTypes().equals(taxRel.getType()) ) {
698
                    if (relationshipEdgeFilter.getDirections().contains(Direction.relatedTo) && !taxa.contains(taxRel.getToTaxon())) {
699
                        if(logger.isDebugEnabled()){
700
                            logger.debug(maxDepth + ": " + taxon.getTitleCache() + " --[" + taxRel.getType().getLabel() + "]--> " + taxRel.getToTaxon().getTitleCache());
701
                        }
702
                        taxa.add(taxRel.getToTaxon());
703
                        if(maxDepth == null || maxDepth > 0) {
704
                            taxa.addAll(collectRelatedTaxa(taxRel.getToTaxon(), includeRelationships, taxa, includeUnpublished, maxDepth));
705
                        }
706
                    }
707
                    if(relationshipEdgeFilter.getDirections().contains(Direction.relatedFrom) && !taxa.contains(taxRel.getFromTaxon())) {
708
                        taxa.add(taxRel.getFromTaxon());
709
                        if(logger.isDebugEnabled()){
710
                            logger.debug(maxDepth + ": " +taxRel.getFromTaxon().getTitleCache() + " --[" + taxRel.getType().getLabel() + "]--> " + taxon.getTitleCache() );
711
                        }
712
                        if(maxDepth == null || maxDepth > 0) {
713
                            taxa.addAll(collectRelatedTaxa(taxRel.getFromTaxon(), includeRelationships, taxa, includeUnpublished, maxDepth));
714
                        }
715
                    }
716
                }
717
            }
718
        }
719
        return taxa;
720
    }
721

    
722
    @Override
723
    public Pager<Synonym> getSynonyms(Taxon taxon,	SynonymType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
724
        Long numberOfResults = dao.countSynonyms(taxon, type);
725

    
726
        List<Synonym> results = new ArrayList<>();
727
        if(numberOfResults > 0) { // no point checking again
728
            results = dao.getSynonyms(taxon, type, pageSize, pageNumber, orderHints, propertyPaths);
729
        }
730

    
731
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
732
    }
733

    
734
    @Override
735
    public List<List<Synonym>> getSynonymsByHomotypicGroup(Taxon taxon, List<String> propertyPaths){
736
        List<List<Synonym>> result = new ArrayList<>();
737
        taxon = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
738
        HomotypicGroupTaxonComparator comparator = new HomotypicGroupTaxonComparator(taxon);
739

    
740

    
741
        //homotypic
742
        result.add(taxon.getHomotypicSynonymsByHomotypicGroup(comparator));
743

    
744
        //heterotypic
745
        List<HomotypicalGroup> homotypicalGroups = taxon.getHeterotypicSynonymyGroups();  //currently the list is sorted by the Taxon.defaultTaxonComparator
746
        for(HomotypicalGroup homotypicalGroup : homotypicalGroups){
747
            result.add(taxon.getSynonymsInGroup(homotypicalGroup, comparator));
748
        }
749

    
750
        return result;
751

    
752
    }
753

    
754
    @Override
755
    public List<Synonym> getHomotypicSynonymsByHomotypicGroup(Taxon taxon, List<String> propertyPaths){
756
        Taxon t = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
757
        HomotypicGroupTaxonComparator comparator = new HomotypicGroupTaxonComparator(taxon);
758

    
759
        return t.getHomotypicSynonymsByHomotypicGroup(comparator);
760
    }
761

    
762
    @Override
763
    public List<List<Synonym>> getHeterotypicSynonymyGroups(Taxon taxon, List<String> propertyPaths){
764
        Taxon t = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
765
        List<HomotypicalGroup> homotypicalGroups = t.getHeterotypicSynonymyGroups();
766
        List<List<Synonym>> heterotypicSynonymyGroups = new ArrayList<>(homotypicalGroups.size());
767
        for(HomotypicalGroup homotypicalGroup : homotypicalGroups){
768
            heterotypicSynonymyGroups.add(t.getSynonymsInGroup(homotypicalGroup));
769
        }
770
        return heterotypicSynonymyGroups;
771
    }
772

    
773
    @Override
774
    public List<UuidAndTitleCache<? extends IdentifiableEntity>> findTaxaAndNamesForEditor(IFindTaxaAndNamesConfigurator config){
775

    
776
        if (config.isDoSynonyms() || config.isDoTaxa() || config.isDoNamesWithoutTaxa() || config.isDoTaxaByCommonNames()){
777
        	return dao.getTaxaByNameForEditor(config.isDoTaxa(), config.isDoSynonyms(), config.isDoNamesWithoutTaxa(),
778
        	        config.isDoMisappliedNames(), config.isDoTaxaByCommonNames(), config.isIncludeUnpublished(),
779
        	        config.getTitleSearchStringSqlized(), config.getClassification(), config.getSubtree(),
780
        	        config.getMatchMode(), config.getNamedAreas(), config.getOrder());
781
        }else{
782
            return new ArrayList<>();
783
        }
784
    }
785

    
786
    @Override
787
    public Pager<IdentifiableEntity> findTaxaAndNames(IFindTaxaAndNamesConfigurator configurator) {
788

    
789
        List<IdentifiableEntity> results = new ArrayList<>();
790
        long numberOfResults = 0; // overall number of results (as opposed to number of results per page)
791
        List<TaxonBase> taxa = null;
792

    
793
        // Taxa and synonyms
794
        long numberTaxaResults = 0L;
795

    
796
        List<String> propertyPath = new ArrayList<>();
797
        if(configurator.getTaxonPropertyPath() != null){
798
            propertyPath.addAll(configurator.getTaxonPropertyPath());
799
        }
800

    
801
        if (configurator.isDoMisappliedNames() || configurator.isDoSynonyms() || configurator.isDoTaxa() || configurator.isDoTaxaByCommonNames()){
802
            if(configurator.getPageSize() != null){ // no point counting if we need all anyway
803
                numberTaxaResults =
804
                    dao.countTaxaByName(configurator.isDoTaxa(),configurator.isDoSynonyms(), configurator.isDoMisappliedNames(),
805
                        configurator.isDoTaxaByCommonNames(), configurator.isDoIncludeAuthors(), configurator.getTitleSearchStringSqlized(),
806
                        configurator.getClassification(), configurator.getSubtree(), configurator.getMatchMode(),
807
                        configurator.getNamedAreas(), configurator.isIncludeUnpublished());
808
            }
809

    
810
            if(configurator.getPageSize() == null || numberTaxaResults > configurator.getPageSize() * configurator.getPageNumber()){ // no point checking again if less results
811
                taxa = dao.getTaxaByName(configurator.isDoTaxa(), configurator.isDoSynonyms(),
812
                    configurator.isDoMisappliedNames(), configurator.isDoTaxaByCommonNames(), configurator.isDoIncludeAuthors(),
813
                    configurator.getTitleSearchStringSqlized(), configurator.getClassification(), configurator.getSubtree(),
814
                    configurator.getMatchMode(), configurator.getNamedAreas(), configurator.isIncludeUnpublished(),
815
                    configurator.getOrder(), configurator.getPageSize(), configurator.getPageNumber(), propertyPath);
816
            }
817
       }
818

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

    
821
        if(taxa != null){
822
            results.addAll(taxa);
823
        }
824

    
825
        numberOfResults += numberTaxaResults;
826

    
827
        // Names without taxa
828
        if (configurator.isDoNamesWithoutTaxa()) {
829
            int numberNameResults = 0;
830

    
831
            List<TaxonName> names =
832
                nameDao.findByName(configurator.isDoIncludeAuthors(), configurator.getTitleSearchStringSqlized(), configurator.getMatchMode(),
833
                        configurator.getPageSize(), configurator.getPageNumber(), null, configurator.getTaxonNamePropertyPath());
834
            if (logger.isDebugEnabled()) { logger.debug(names.size() + " matching name(s) found"); }
835
            if (names.size() > 0) {
836
                for (TaxonName taxonName : names) {
837
                    if (taxonName.getTaxonBases().size() == 0) {
838
                        results.add(taxonName);
839
                        numberNameResults++;
840
                    }
841
                }
842
                if (logger.isDebugEnabled()) { logger.debug(numberNameResults + " matching name(s) without taxa found"); }
843
                numberOfResults += numberNameResults;
844
            }
845
        }
846

    
847
       return new DefaultPagerImpl<> (configurator.getPageNumber(), numberOfResults, configurator.getPageSize(), results);
848
    }
849

    
850
    public List<UuidAndTitleCache<TaxonBase>> getTaxonUuidAndTitleCache(Integer limit, String pattern){
851
        return dao.getUuidAndTitleCache(limit, pattern);
852
    }
853

    
854
    @Override
855
    public List<Media> listTaxonDescriptionMedia(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, boolean limitToGalleries, List<String> propertyPath){
856
        return listMedia(taxon, includeRelationships, limitToGalleries, true, false, false, propertyPath);
857
    }
858

    
859
    @Override
860
    public List<Media> listMedia(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships,
861
            Boolean limitToGalleries, Boolean includeTaxonDescriptions, Boolean includeOccurrences,
862
            Boolean includeTaxonNameDescriptions, List<String> propertyPath) {
863

    
864
        //TODO let inherit
865
        boolean includeUnpublished = INCLUDE_UNPUBLISHED;
866

    
867
    //    logger.setLevel(Level.TRACE);
868
//        Logger.getLogger("org.hibernate.SQL").setLevel(Level.TRACE);
869

    
870
        logger.trace("listMedia() - START");
871

    
872
        Set<Taxon> taxa = new HashSet<>();
873
        List<Media> taxonMedia = new ArrayList<>();
874
        List<Media> nonImageGalleryImages = new ArrayList<>();
875

    
876
        if (limitToGalleries == null) {
877
            limitToGalleries = false;
878
        }
879

    
880
        // --- resolve related taxa
881
        if (includeRelationships != null && ! includeRelationships.isEmpty()) {
882
            logger.trace("listMedia() - resolve related taxa");
883
            taxa = listRelatedTaxa(taxon, includeRelationships, null, includeUnpublished, null, null, null);
884
        }
885

    
886
        taxa.add((Taxon) dao.load(taxon.getUuid()));
887

    
888
        if(includeTaxonDescriptions != null && includeTaxonDescriptions){
889
            logger.trace("listMedia() - includeTaxonDescriptions");
890
            List<TaxonDescription> taxonDescriptions = new ArrayList<>();
891
            // --- TaxonDescriptions
892
            for (Taxon t : taxa) {
893
                taxonDescriptions.addAll(descriptionService.listTaxonDescriptions(t, null, null, null, null, propertyPath));
894
            }
895
            for (TaxonDescription taxonDescription : taxonDescriptions) {
896
                if (!limitToGalleries || taxonDescription.isImageGallery()) {
897
                    for (DescriptionElementBase element : taxonDescription.getElements()) {
898
                        for (Media media : element.getMedia()) {
899
                            if(taxonDescription.isImageGallery()){
900
                                taxonMedia.add(media);
901
                            }
902
                            else{
903
                                nonImageGalleryImages.add(media);
904
                            }
905
                        }
906
                    }
907
                }
908
            }
909
            //put images from image gallery first (#3242)
910
            taxonMedia.addAll(nonImageGalleryImages);
911
        }
912

    
913

    
914
        if(includeOccurrences != null && includeOccurrences) {
915
            logger.trace("listMedia() - includeOccurrences");
916
            Set<SpecimenOrObservationBase> specimensOrObservations = new HashSet<>();
917
            // --- Specimens
918
            for (Taxon t : taxa) {
919
                specimensOrObservations.addAll(occurrenceDao.listByAssociatedTaxon(null, t, null, null, null, null));
920
            }
921
            for (SpecimenOrObservationBase<?> occurrence : specimensOrObservations) {
922

    
923
//            	direct media removed from specimen #3597
924
//              taxonMedia.addAll(occurrence.getMedia());
925

    
926
                // SpecimenDescriptions
927
                Set<SpecimenDescription> specimenDescriptions = occurrence.getSpecimenDescriptions();
928
                for (DescriptionBase<?> specimenDescription : specimenDescriptions) {
929
                    if (!limitToGalleries || specimenDescription.isImageGallery()) {
930
                        Set<DescriptionElementBase> elements = specimenDescription.getElements();
931
                        for (DescriptionElementBase element : elements) {
932
                            for (Media media : element.getMedia()) {
933
                                taxonMedia.add(media);
934
                            }
935
                        }
936
                    }
937
                }
938

    
939
                if (occurrence.isInstanceOf(DerivedUnit.class)) {
940
                    DerivedUnit derivedUnit = CdmBase.deproxy(occurrence, DerivedUnit.class);
941
                    // Collection
942
                    //TODO why may collections have media attached? #
943
                    if (derivedUnit.getCollection() != null){
944
                        taxonMedia.addAll(derivedUnit.getCollection().getMedia());
945
                    }
946
                }
947
                //media in hierarchy
948
                taxonMedia.addAll(occurrenceService.getMediainHierarchy(occurrence, null, null, propertyPath).getRecords());
949
            }
950
        }
951

    
952
        if(includeTaxonNameDescriptions != null && includeTaxonNameDescriptions) {
953
            logger.trace("listMedia() - includeTaxonNameDescriptions");
954
            // --- TaxonNameDescription
955
            Set<TaxonNameDescription> nameDescriptions = new HashSet<>();
956
            for (Taxon t : taxa) {
957
                nameDescriptions .addAll(t.getName().getDescriptions());
958
            }
959
            for(TaxonNameDescription nameDescription: nameDescriptions){
960
                if (!limitToGalleries || nameDescription.isImageGallery()) {
961
                    Set<DescriptionElementBase> elements = nameDescription.getElements();
962
                    for (DescriptionElementBase element : elements) {
963
                        for (Media media : element.getMedia()) {
964
                            taxonMedia.add(media);
965
                        }
966
                    }
967
                }
968
            }
969
        }
970

    
971

    
972
        logger.trace("listMedia() - initialize");
973
        beanInitializer.initializeAll(taxonMedia, propertyPath);
974

    
975
        logger.trace("listMedia() - END");
976

    
977
        return taxonMedia;
978
    }
979

    
980
    @Override
981
    public List<TaxonBase> findTaxaByID(Set<Integer> listOfIDs) {
982
        return this.dao.loadList(listOfIDs, null, null);
983
    }
984

    
985
    @Override
986
    public TaxonBase findTaxonByUuid(UUID uuid, List<String> propertyPaths){
987
        return this.dao.findByUuid(uuid, null ,propertyPaths);
988
    }
989

    
990
    @Override
991
    public long countSynonyms(boolean onlyAttachedToTaxon){
992
        return this.dao.countSynonyms(onlyAttachedToTaxon);
993
    }
994

    
995

    
996

    
997
    @Override
998
    @Transactional(readOnly=false)
999
    public DeleteResult deleteTaxon(UUID taxonUUID, TaxonDeletionConfigurator config, UUID classificationUuid)  {
1000

    
1001
    	if (config == null){
1002
            config = new TaxonDeletionConfigurator();
1003
        }
1004
    	Taxon taxon = (Taxon)dao.load(taxonUUID);
1005
    	DeleteResult result = new DeleteResult();
1006
    	if (taxon == null){
1007
    	    result.setAbort();
1008
    	    result.addException(new Exception ("The taxon was already deleted."));
1009
    	    return result;
1010
    	}
1011
    	taxon = HibernateProxyHelper.deproxy(taxon);
1012
    	Classification classification = HibernateProxyHelper.deproxy(classificationDao.load(classificationUuid), Classification.class);
1013
    	config.setClassificationUuid(classificationUuid);
1014
        result = isDeletable(taxonUUID, config);
1015

    
1016
        if (result.isOk()){
1017
            // --- DeleteSynonymRelations
1018
            if (config.isDeleteSynonymRelations()){
1019
                boolean removeSynonymNameFromHomotypicalGroup = false;
1020
                // use tmp Set to avoid concurrent modification
1021
                Set<Synonym> synsToDelete = new HashSet<>();
1022
                synsToDelete.addAll(taxon.getSynonyms());
1023
                for (Synonym synonym : synsToDelete){
1024
                    taxon.removeSynonym(synonym, removeSynonymNameFromHomotypicalGroup);
1025

    
1026
                    // --- DeleteSynonymsIfPossible
1027
                    if (config.isDeleteSynonymsIfPossible()){
1028
                        //TODO which value
1029
                        boolean newHomotypicGroupIfNeeded = true;
1030
                        SynonymDeletionConfigurator synConfig = new SynonymDeletionConfigurator();
1031
                        result.includeResult(deleteSynonym(synonym, synConfig));
1032
                    }
1033
                }
1034
            }
1035

    
1036
            // --- DeleteTaxonRelationships
1037
            if (! config.isDeleteTaxonRelationships()){
1038
                if (taxon.getTaxonRelations().size() > 0){
1039
                    result.setAbort();
1040
                    result.addException(new Exception("Taxon can't be deleted as it is related to another taxon. " +
1041
                            "Remove taxon from all relations to other taxa prior to deletion."));
1042
                }
1043
            } else{
1044
                TaxonDeletionConfigurator configRelTaxon = new TaxonDeletionConfigurator();
1045
                configRelTaxon.setDeleteTaxonNodes(false);
1046
                configRelTaxon.setDeleteConceptRelationships(true);
1047

    
1048
                for (TaxonRelationship taxRel: taxon.getTaxonRelations()){
1049
                    if (config.isDeleteMisappliedNamesAndInvalidDesignations()
1050
                            && taxRel.getType().isMisappliedNameOrInvalidDesignation()
1051
                            && taxon.equals(taxRel.getToTaxon())){
1052
                        this.deleteTaxon(taxRel.getFromTaxon().getUuid(), config, classificationUuid);
1053
                    } else if (config.isDeleteConceptRelationships() && taxRel.getType().isConceptRelationship()){
1054

    
1055
                        if (taxon.equals(taxRel.getToTaxon()) && isDeletable(taxRel.getFromTaxon().getUuid(), configRelTaxon).isOk()){
1056
                            this.deleteTaxon(taxRel.getFromTaxon().getUuid(), configRelTaxon, classificationUuid);
1057
                        }else if (isDeletable(taxRel.getToTaxon().getUuid(), configRelTaxon).isOk()){
1058
                            this.deleteTaxon(taxRel.getToTaxon().getUuid(), configRelTaxon, classificationUuid);
1059
                        }
1060
                    }
1061
                    taxon.removeTaxonRelation(taxRel);
1062
                }
1063
            }
1064

    
1065
            //    	TaxonDescription
1066
            if (config.isDeleteDescriptions()){
1067
                Set<TaxonDescription> descriptions = taxon.getDescriptions();
1068
                List<TaxonDescription> removeDescriptions = new ArrayList<>();
1069
                for (TaxonDescription desc: descriptions){
1070
                    //TODO use description delete configurator ?
1071
                    //FIXME check if description is ALWAYS deletable
1072
                    if (desc.getDescribedSpecimenOrObservation() != null){
1073
                        result.setAbort();
1074
                        result.addException(new Exception("Taxon can't be deleted as it is used in a TaxonDescription" +
1075
                                " which also describes specimens or observations"));
1076
                        break;
1077
                    }
1078
                    removeDescriptions.add(desc);
1079

    
1080

    
1081
                }
1082
                if (result.isOk()){
1083
                    for (TaxonDescription desc: removeDescriptions){
1084
                        taxon.removeDescription(desc);
1085
                        descriptionService.delete(desc);
1086
                    }
1087
                } else {
1088
                    return result;
1089
                }
1090
            }
1091

    
1092

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

    
1156
         if ((taxon.getTaxonNodes() == null || taxon.getTaxonNodes().size()== 0)  && result.isOk()){
1157
             try{
1158
                 dao.delete(taxon);
1159
                 result.addDeletedObject(taxon);
1160
             }catch(Exception e){
1161
                 result.addException(e);
1162
                 result.setError();
1163
             }
1164
         } else {
1165
             result.setError();
1166
             result.addException(new Exception("The Taxon can't be deleted because it is used in a classification."));
1167

    
1168
         }
1169
         //TaxonName
1170
         if (config.isDeleteNameIfPossible() && result.isOk()){
1171
            DeleteResult nameResult = new DeleteResult();
1172
            //remove name if possible (and required)
1173
            if (name != null ){
1174
                nameResult = nameService.delete(name.getUuid(), config.getNameDeletionConfig());
1175
            }
1176
            if (nameResult.isError() || nameResult.isAbort()){
1177
                result.addRelatedObject(name);
1178
                result.addExceptions(nameResult.getExceptions());
1179
            }else{
1180
                result.includeResult(nameResult);
1181
            }
1182
         }
1183
       }
1184

    
1185
       return result;
1186

    
1187
    }
1188

    
1189
    @Override
1190
    @Transactional(readOnly = false)
1191
    public DeleteResult delete(UUID synUUID){
1192
    	Synonym syn = (Synonym)dao.load(synUUID);
1193
        return this.deleteSynonym(syn, null);
1194
    }
1195

    
1196
    @Override
1197
    @Transactional(readOnly = false)
1198
    public DeleteResult deleteSynonym(UUID synonymUuid, SynonymDeletionConfigurator config) {
1199
        return deleteSynonym((Synonym)dao.load(synonymUuid), config);
1200

    
1201
    }
1202

    
1203

    
1204
    @Override
1205
    @Transactional(readOnly = false)
1206
    public DeleteResult deleteSynonym(Synonym synonym, SynonymDeletionConfigurator config) {
1207
        DeleteResult result = new DeleteResult();
1208
    	if (synonym == null){
1209
    		result.setAbort();
1210
    		result.addException(new Exception("The synonym was already deleted."));
1211
    		return result;
1212
        }
1213

    
1214
        if (config == null){
1215
            config = new SynonymDeletionConfigurator();
1216
        }
1217

    
1218
        result = isDeletable(synonym.getUuid(), config);
1219

    
1220
        if (result.isOk()){
1221

    
1222
            synonym = HibernateProxyHelper.deproxy(this.load(synonym.getUuid()), Synonym.class);
1223

    
1224
            //remove synonym
1225
            Taxon accTaxon = synonym.getAcceptedTaxon();
1226

    
1227
            if (accTaxon != null){
1228
                accTaxon = HibernateProxyHelper.deproxy(accTaxon, Taxon.class);
1229
                accTaxon.removeSynonym(synonym, false);
1230
                this.saveOrUpdate(accTaxon);
1231
                result.addUpdatedObject(accTaxon);
1232
            }
1233
            this.saveOrUpdate(synonym);
1234
            //#6281
1235
            dao.flush();
1236

    
1237
            TaxonName name = synonym.getName();
1238
            synonym.setName(null);
1239

    
1240
            dao.delete(synonym);
1241
            result.addDeletedObject(synonym);
1242

    
1243
            //remove name if possible (and required)
1244
            if (name != null && config.isDeleteNameIfPossible()){
1245

    
1246
                    DeleteResult nameDeleteResult = nameService.delete(name, config.getNameDeletionConfig());
1247
                    if (nameDeleteResult.isAbort() || nameDeleteResult.isError()){
1248
                    	result.addExceptions(nameDeleteResult.getExceptions());
1249
                    	result.addRelatedObject(name);
1250
                    }else{
1251
                        result.addDeletedObject(name);
1252
                    }
1253
            }
1254

    
1255
        }
1256
        return result;
1257
    }
1258

    
1259
    @Override
1260
    public Map<String, Map<UUID,Set<TaxonName>>> findIdenticalTaxonNames(List<UUID> sourceRefUuids, List<String> propertyPaths) {
1261
        return this.dao.findIdenticalNames(sourceRefUuids, propertyPaths);
1262
    }
1263

    
1264

    
1265
    @Override
1266
    public Taxon findBestMatchingTaxon(String taxonName) {
1267
        MatchingTaxonConfigurator config = MatchingTaxonConfigurator.NewInstance();
1268
        config.setTaxonNameTitle(taxonName);
1269
        return findBestMatchingTaxon(config);
1270
    }
1271

    
1272
    @Override
1273
    public Taxon findBestMatchingTaxon(MatchingTaxonConfigurator config) {
1274

    
1275
        Taxon bestCandidate = null;
1276
        try{
1277
            // 1. search for accepted taxa
1278
            List<TaxonBase> taxonList = dao.findByNameTitleCache(true, false, config.isIncludeUnpublished(),
1279
                    config.getTaxonNameTitle(), null, null, MatchMode.EXACT, null, null, 0, null, null);
1280
            boolean bestCandidateMatchesSecUuid = false;
1281
            boolean bestCandidateIsInClassification = false;
1282
            int countEqualCandidates = 0;
1283
            for(TaxonBase<?> taxonBaseCandidate : taxonList){
1284
                if(taxonBaseCandidate instanceof Taxon){
1285
                    Taxon newCanditate = CdmBase.deproxy(taxonBaseCandidate, Taxon.class);
1286
                    boolean newCandidateMatchesSecUuid = isMatchesSecUuid(newCanditate, config);
1287
                    if (! newCandidateMatchesSecUuid && config.isOnlyMatchingSecUuid() ){
1288
                        continue;
1289
                    }else if(newCandidateMatchesSecUuid && ! bestCandidateMatchesSecUuid){
1290
                        bestCandidate = newCanditate;
1291
                        countEqualCandidates = 1;
1292
                        bestCandidateMatchesSecUuid = true;
1293
                        continue;
1294
                    }
1295

    
1296
                    boolean newCandidateInClassification = isInClassification(newCanditate, config);
1297
                    if (! newCandidateInClassification && config.isOnlyMatchingClassificationUuid()){
1298
                        continue;
1299
                    }else if (newCandidateInClassification && ! bestCandidateIsInClassification){
1300
                        bestCandidate = newCanditate;
1301
                        countEqualCandidates = 1;
1302
                        bestCandidateIsInClassification = true;
1303
                        continue;
1304
                    }
1305
                    if (bestCandidate == null){
1306
                        bestCandidate = newCanditate;
1307
                        countEqualCandidates = 1;
1308
                        continue;
1309
                    }
1310

    
1311
                }else{  //not Taxon.class
1312
                    continue;
1313
                }
1314
                countEqualCandidates++;
1315

    
1316
            }
1317
            if (bestCandidate != null){
1318
                if(countEqualCandidates > 1){
1319
                    logger.info(countEqualCandidates + " equally matching TaxonBases found, using first accepted Taxon: " + bestCandidate.getTitleCache());
1320
                    return bestCandidate;
1321
                } else {
1322
                    logger.info("using accepted Taxon: " + bestCandidate.getTitleCache());
1323
                    return bestCandidate;
1324
                }
1325
            }
1326

    
1327

    
1328
            // 2. search for synonyms
1329
            if (config.isIncludeSynonyms()){
1330
                List<TaxonBase> synonymList = dao.findByNameTitleCache(false, true, config.isIncludeUnpublished(),
1331
                        config.getTaxonNameTitle(), null, null, MatchMode.EXACT, null, null, 0, null, null);
1332
                for(TaxonBase taxonBase : synonymList){
1333
                    if(taxonBase instanceof Synonym){
1334
                        Synonym synonym = CdmBase.deproxy(taxonBase, Synonym.class);
1335
                        bestCandidate = synonym.getAcceptedTaxon();
1336
                        if(bestCandidate != null){
1337
                            logger.info("using accepted Taxon " +  bestCandidate.getTitleCache() + " for synonym " + taxonBase.getTitleCache());
1338
                            return bestCandidate;
1339
                        }
1340
                        //TODO extend method: search using treeUUID, using SecUUID, first find accepted then include synonyms until a matching taxon is found
1341
                    }
1342
                }
1343
            }
1344

    
1345
        } catch (Exception e){
1346
            logger.error(e);
1347
            e.printStackTrace();
1348
        }
1349

    
1350
        return bestCandidate;
1351
    }
1352

    
1353
    private boolean isInClassification(Taxon taxon, MatchingTaxonConfigurator config) {
1354
        UUID configClassificationUuid = config.getClassificationUuid();
1355
        if (configClassificationUuid == null){
1356
            return false;
1357
        }
1358
        for (TaxonNode node : taxon.getTaxonNodes()){
1359
            UUID classUuid = node.getClassification().getUuid();
1360
            if (configClassificationUuid.equals(classUuid)){
1361
                return true;
1362
            }
1363
        }
1364
        return false;
1365
    }
1366

    
1367
    private boolean isMatchesSecUuid(Taxon taxon, MatchingTaxonConfigurator config) {
1368
        UUID configSecUuid = config.getSecUuid();
1369
        if (configSecUuid == null){
1370
            return false;
1371
        }
1372
        UUID taxonSecUuid = (taxon.getSec() == null)? null : taxon.getSec().getUuid();
1373
        return configSecUuid.equals(taxonSecUuid);
1374
    }
1375

    
1376
    @Override
1377
    public Synonym findBestMatchingSynonym(String taxonName, boolean includeUnpublished) {
1378
        List<TaxonBase> synonymList = dao.findByNameTitleCache(false, true, includeUnpublished, taxonName, null, null, MatchMode.EXACT, null, null, 0, null, null);
1379
        if(! synonymList.isEmpty()){
1380
            Synonym result = CdmBase.deproxy(synonymList.iterator().next(), Synonym.class);
1381
            if(synonymList.size() == 1){
1382
                logger.info(synonymList.size() + " Synonym found " + result.getTitleCache() );
1383
                return result;
1384
            } else {
1385
                logger.info("Several matching synonyms found. Using first: " +  result.getTitleCache());
1386
                return result;
1387
            }
1388
        }
1389
        return null;
1390
    }
1391

    
1392
    @Override
1393
    @Transactional(readOnly = false)
1394
    public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym,
1395
            Taxon newTaxon,
1396
            boolean moveHomotypicGroup,
1397
            SynonymType newSynonymType) throws HomotypicalGroupChangeException {
1398
        return moveSynonymToAnotherTaxon(oldSynonym, newTaxon, moveHomotypicGroup,
1399
                newSynonymType,
1400
                oldSynonym.getSec(),
1401
                oldSynonym.getSecMicroReference(),
1402
                true);
1403
    }
1404

    
1405
    @Override
1406
    @Transactional(readOnly = false)
1407
    public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym,
1408
            Taxon newTaxon,
1409
            boolean moveHomotypicGroup,
1410
            SynonymType newSynonymType,
1411
            Reference newSecundum,
1412
            String newSecundumDetail,
1413
            boolean keepSecundumIfUndefined) throws HomotypicalGroupChangeException {
1414

    
1415
        Synonym synonym = CdmBase.deproxy(dao.load(oldSynonym.getUuid()), Synonym.class);
1416
        Taxon oldTaxon = CdmBase.deproxy(dao.load(synonym.getAcceptedTaxon().getUuid()), Taxon.class);
1417
        //TODO what if there is no name ?? Concepts may be cached (e.g. via TCS import)
1418
        TaxonName synonymName = synonym.getName();
1419
        TaxonName fromTaxonName = oldTaxon.getName();
1420
        //set default relationship type
1421
        if (newSynonymType == null){
1422
            newSynonymType = SynonymType.HETEROTYPIC_SYNONYM_OF();
1423
        }
1424
        boolean newRelTypeIsHomotypic = newSynonymType.equals(SynonymType.HOMOTYPIC_SYNONYM_OF());
1425

    
1426
        HomotypicalGroup homotypicGroup = synonymName.getHomotypicalGroup();
1427
        int hgSize = homotypicGroup.getTypifiedNames().size();
1428
        boolean isSingleInGroup = !(hgSize > 1);
1429

    
1430
        if (! isSingleInGroup){
1431
            boolean isHomotypicToAccepted = synonymName.isHomotypic(fromTaxonName);
1432
            boolean hasHomotypicSynonymRelatives = isHomotypicToAccepted ? hgSize > 2 : hgSize > 1;
1433
            if (isHomotypicToAccepted){
1434
                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.";
1435
                String homotypicRelatives = hasHomotypicSynonymRelatives ? " and other synonym(s)":"";
1436
                message = String.format(message, homotypicRelatives);
1437
                throw new HomotypicalGroupChangeException(message);
1438
            }
1439
            if (! moveHomotypicGroup){
1440
                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.";
1441
                throw new HomotypicalGroupChangeException(message);
1442
            }
1443
        }else{
1444
            moveHomotypicGroup = true;  //single synonym always allows to moveCompleteGroup
1445
        }
1446
//        Assert.assertTrue("Synonym can only be moved with complete homotypic group", moveHomotypicGroup);
1447

    
1448
        UpdateResult result = new UpdateResult();
1449
        //move all synonyms to new taxon
1450
        List<Synonym> homotypicSynonyms = oldTaxon.getSynonymsInGroup(homotypicGroup);
1451
        for (Synonym synRelation: homotypicSynonyms){
1452

    
1453
            newTaxon = HibernateProxyHelper.deproxy(newTaxon, Taxon.class);
1454
            oldTaxon = HibernateProxyHelper.deproxy(oldTaxon, Taxon.class);
1455
            oldTaxon.removeSynonym(synRelation, false);
1456
            newTaxon.addSynonym(synRelation, newSynonymType);
1457

    
1458
            if (newSecundum != null || !keepSecundumIfUndefined){
1459
                synRelation.setSec(newSecundum);
1460
            }
1461
            if (newSecundumDetail != null || !keepSecundumIfUndefined){
1462
                synRelation.setSecMicroReference(newSecundumDetail);
1463
            }
1464

    
1465
            //set result  //why is this needed? Seems wrong to me (AM 10.10.2016)
1466
            if (!synRelation.equals(oldSynonym)){
1467
                result.setError();
1468
            }
1469
        }
1470

    
1471
        result.addUpdatedObject(oldTaxon);
1472
        result.addUpdatedObject(newTaxon);
1473
        saveOrUpdate(oldTaxon);
1474
        saveOrUpdate(newTaxon);
1475

    
1476
        return result;
1477
    }
1478

    
1479
    @Override
1480
    public <T extends TaxonBase> List<UuidAndTitleCache<T>> getUuidAndTitleCache(Class<T> clazz, Integer limit, String pattern) {
1481
        return dao.getUuidAndTitleCache(clazz, limit, pattern);
1482
    }
1483

    
1484
    @Override
1485
    public Pager<SearchResult<TaxonBase>> findByFullText(
1486
            Class<? extends TaxonBase> clazz, String queryString,
1487
            Classification classification, TaxonNode subtree, boolean includeUnpublished, List<Language> languages,
1488
            boolean highlightFragments, Integer pageSize, Integer pageNumber,
1489
            List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
1490

    
1491
        LuceneSearch luceneSearch = prepareFindByFullTextSearch(clazz, queryString, classification, subtree,
1492
                null, includeUnpublished, languages, highlightFragments, null);
1493

    
1494
        // --- execute search
1495
        TopGroups<BytesRef> topDocsResultSet;
1496
        try {
1497
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
1498
        } catch (ParseException e) {
1499
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
1500
            luceneParseException.setStackTrace(e.getStackTrace());
1501
            throw luceneParseException;
1502
        }
1503

    
1504
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1505
        idFieldMap.put(CdmBaseType.TAXON, "id");
1506

    
1507
        // ---  initialize taxa, thighlight matches ....
1508
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1509
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1510
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1511

    
1512
        long totalHits = topDocsResultSet != null ? Long.valueOf(topDocsResultSet.totalGroupCount) : 0;
1513
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
1514
    }
1515

    
1516

    
1517

    
1518
    @Transactional(readOnly = true)
1519
    @Override
1520
    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) {
1521
         long numberOfResults = dao.countByTitleWithRestrictions(clazz, queryString, matchmode, restrictions);
1522
         numberOfResults = numberOfResults + dao.countByTitleWithRestrictions(clazz, "?".concat(queryString), matchmode, restrictions);
1523

    
1524
         List<S> results = new ArrayList<>();
1525
         if(numberOfResults > 0) { // no point checking again //TODO use AbstractPagerImpl.hasResultsInRange(numberOfResults, pageNumber, pageSize)
1526

    
1527
             results = dao.findByTitleWithRestrictions(clazz, queryString, matchmode, restrictions, pageSize, pageNumber, orderHints, propertyPaths);
1528
             results.addAll(dao.findByTitleWithRestrictions(clazz, "?".concat(queryString), matchmode, restrictions, pageSize, pageNumber, orderHints, propertyPaths));
1529
         }
1530

    
1531
         return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
1532
    }
1533

    
1534

    
1535
    @Transactional(readOnly = true)
1536
    @Override
1537
    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) {
1538
        long numberOfResults = dao.countByTitle(clazz, queryString, matchmode, criteria);
1539
        //check whether there are doubtful taxa matching
1540
        long numberOfResults_doubtful = dao.countByTitle(clazz, "?".concat(queryString), matchmode, criteria);
1541
        List<S> results = new ArrayList<>();
1542
        if(numberOfResults > 0 || numberOfResults_doubtful > 0) { // no point checking again //TODO use AbstractPagerImpl.hasResultsInRange(numberOfResults, pageNumber, pageSize)
1543
               if (numberOfResults > 0){
1544
                   results = dao.findByTitle(clazz, queryString, matchmode, criteria, pageSize, pageNumber, orderHints, propertyPaths);
1545
               }else{
1546
                   results = new ArrayList<>();
1547
               }
1548
               if (numberOfResults_doubtful > 0){
1549
                   results.addAll(dao.findByTitle(clazz, "?".concat(queryString), matchmode,  criteria, pageSize, pageNumber, orderHints, propertyPaths));
1550
               }
1551
        }
1552

    
1553
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
1554
    }
1555

    
1556

    
1557
    @Override
1558
    public Pager<SearchResult<TaxonBase>> findByDistribution(List<NamedArea> areaFilter, List<PresenceAbsenceTerm> statusFilter,
1559
            Classification classification, TaxonNode subtree,
1560
            Integer pageSize, Integer pageNumber,
1561
            List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
1562

    
1563
        LuceneSearch luceneSearch = prepareByDistributionSearch(areaFilter, statusFilter, classification, subtree);
1564

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

    
1575
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1576
        idFieldMap.put(CdmBaseType.TAXON, "id");
1577

    
1578
        // ---  initialize taxa, thighlight matches ....
1579
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1580
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1581
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1582

    
1583
        long totalHits = topDocsResultSet != null ? Long.valueOf(topDocsResultSet.totalGroupCount) : 0;
1584
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
1585
    }
1586

    
1587
    /**
1588
     * @param clazz
1589
     * @param queryString
1590
     * @param classification
1591
     * @param includeUnpublished
1592
     * @param languages
1593
     * @param highlightFragments
1594
     * @param sortFields TODO
1595
     * @param directorySelectClass
1596
     * @return
1597
     */
1598
    protected LuceneSearch prepareFindByFullTextSearch(Class<? extends CdmBase> clazz, String queryString,
1599
            Classification classification, TaxonNode subtree, String className, boolean includeUnpublished, List<Language> languages,
1600
            boolean highlightFragments, SortField[] sortFields) {
1601

    
1602
        Builder finalQueryBuilder = new Builder();
1603
        Builder textQueryBuilder = new Builder();
1604

    
1605
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1606
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1607

    
1608
        if(sortFields == null){
1609
            sortFields = new SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING, false)};
1610
        }
1611
        luceneSearch.setSortFields(sortFields);
1612

    
1613
        // ---- search criteria
1614
        luceneSearch.setCdmTypRestriction(clazz);
1615

    
1616
        if(!StringUtils.isEmpty(queryString) && !queryString.equals("*") && !queryString.equals("?") ) {
1617
            textQueryBuilder.add(taxonBaseQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
1618
            textQueryBuilder.add(taxonBaseQueryFactory.newDefinedTermQuery("name.rank", queryString, languages), Occur.SHOULD);
1619
        }
1620
        if(className != null){
1621
            textQueryBuilder.add(taxonBaseQueryFactory.newTermQuery("classInfo.name", className, false), Occur.MUST);
1622
        }
1623

    
1624
        BooleanQuery textQuery = textQueryBuilder.build();
1625
        if(textQuery.clauses().size() > 0) {
1626
            finalQueryBuilder.add(textQuery, Occur.MUST);
1627
        }
1628

    
1629
        if(classification != null){
1630
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery(AcceptedTaxonBridge.DOC_KEY_CLASSIFICATION_ID, classification), Occur.MUST);
1631
        }
1632
        if(subtree != null){
1633
            finalQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(AcceptedTaxonBridge.DOC_KEY_TREEINDEX, subtree.treeIndexWc(), true), Occur.MUST);
1634
        }
1635
        if(!includeUnpublished)  {
1636
            String accPublishParam = TaxonBase.ACC_TAXON_BRIDGE_PREFIX + AcceptedTaxonBridge.DOC_KEY_PUBLISH_SUFFIX;
1637
            finalQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery(accPublishParam, true), Occur.MUST);
1638
            finalQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery("publish", true), Occur.MUST);
1639
        }
1640

    
1641
        luceneSearch.setQuery(finalQueryBuilder.build());
1642

    
1643
        if(highlightFragments){
1644
            luceneSearch.setHighlightFields(taxonBaseQueryFactory.getTextFieldNamesAsArray());
1645
        }
1646
        return luceneSearch;
1647
    }
1648

    
1649
    /**
1650
     * Uses org.apache.lucene.search.join.JoinUtil for query time joining, alternatively
1651
     * the BlockJoinQuery could be used. The latter might be more memory save but has the
1652
     * drawback of requiring to do the join an indexing time.
1653
     * see  http://dev.e-taxonomy.eu/trac/wiki/LuceneNotes#JoinsinLucene for more information on this.
1654
     *
1655
     * Joins TaxonRelationShip with Taxon depending on the direction of the given edge:
1656
     * <ul>
1657
     * <li>direct, everted: {@link Direction.relatedTo}: TaxonRelationShip.relatedTo.id --&gt; Taxon.id </li>
1658
     * <li>inverse: {@link Direction.relatedFrom}:  TaxonRelationShip.relatedFrom.id --&gt; Taxon.id </li>
1659
     * <ul>
1660
     * @param queryString
1661
     * @param classification
1662
     * @param languages
1663
     * @param highlightFragments
1664
     * @param sortFields TODO
1665
     *
1666
     * @return
1667
     * @throws IOException
1668
     */
1669
    protected LuceneSearch prepareFindByTaxonRelationFullTextSearch(TaxonRelationshipEdge edge, String queryString,
1670
            Classification classification, TaxonNode subtree, boolean includeUnpublished, List<Language> languages,
1671
            boolean highlightFragments, SortField[] sortFields) throws IOException {
1672

    
1673
        String fromField;
1674
        String queryTermField;
1675
        String toField = "id"; // TaxonBase.uuid
1676
        String publishField;
1677
        String publishFieldInvers;
1678

    
1679
        if(edge.isBidirectional()){
1680
            throw new RuntimeException("Bidirectional joining not supported!");
1681
        }
1682
        if(edge.isEvers()){
1683
            fromField = "relatedFrom.id";
1684
            queryTermField = "relatedFrom.titleCache";
1685
            publishField = "relatedFrom.publish";
1686
            publishFieldInvers = "relatedTo.publish";
1687
        } else if(edge.isInvers()) {
1688
            fromField = "relatedTo.id";
1689
            queryTermField = "relatedTo.titleCache";
1690
            publishField = "relatedTo.publish";
1691
            publishFieldInvers = "relatedFrom.publish";
1692
        } else {
1693
            throw new RuntimeException("Invalid direction: " + edge.getDirections());
1694
        }
1695

    
1696
        Builder finalQueryBuilder = new Builder();
1697

    
1698
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1699
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1700

    
1701
        Builder joinFromQueryBuilder = new Builder();
1702
        if(!StringUtils.isEmpty(queryString)){
1703
            joinFromQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(queryTermField, queryString), Occur.MUST);
1704
        }
1705
        joinFromQueryBuilder.add(taxonBaseQueryFactory.newEntityIdsQuery("type.id", edge.getRelationshipTypes()), Occur.MUST);
1706
        if(!includeUnpublished){
1707
            joinFromQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery(publishField, true), Occur.MUST);
1708
            joinFromQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery(publishFieldInvers, true), Occur.MUST);
1709
        }
1710

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

    
1713
        if(sortFields == null){
1714
            sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING,  false)};
1715
        }
1716
        luceneSearch.setSortFields(sortFields);
1717

    
1718
        finalQueryBuilder.add(joinQuery, Occur.MUST);
1719

    
1720
        if(classification != null){
1721
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery(AcceptedTaxonBridge.DOC_KEY_CLASSIFICATION_ID, classification), Occur.MUST);
1722
        }
1723
        if(subtree != null){
1724
            finalQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(AcceptedTaxonBridge.DOC_KEY_TREEINDEX, subtree.treeIndexWc(), true), Occur.MUST);
1725
        }
1726

    
1727
        luceneSearch.setQuery(finalQueryBuilder.build());
1728

    
1729
        if(highlightFragments){
1730
            luceneSearch.setHighlightFields(taxonBaseQueryFactory.getTextFieldNamesAsArray());
1731
        }
1732
        return luceneSearch;
1733
    }
1734

    
1735
    @Override
1736
    public Pager<SearchResult<TaxonBase>> findTaxaAndNamesByFullText(
1737
            EnumSet<TaxaAndNamesSearchMode> searchModes, String queryString,
1738
            Classification classification, TaxonNode subtree,
1739
            Set<NamedArea> namedAreas, Set<PresenceAbsenceTerm> distributionStatus, List<Language> languages,
1740
            boolean highlightFragments, Integer pageSize,
1741
            Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths)
1742
            throws IOException, LuceneParseException, LuceneMultiSearchException {
1743

    
1744
        // FIXME: allow taxonomic ordering
1745
        //  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";
1746
        // this require building a special sort column by a special classBridge
1747
        if(highlightFragments){
1748
            logger.warn("findTaxaAndNamesByFullText() : fragment highlighting is " +
1749
                    "currently not fully supported by this method and thus " +
1750
                    "may not work with common names and misapplied names.");
1751
        }
1752

    
1753
        // convert sets to lists
1754
        List<NamedArea> namedAreaList = null;
1755
        List<PresenceAbsenceTerm> distributionStatusList = null;
1756
        if(namedAreas != null){
1757
            namedAreaList = new ArrayList<>(namedAreas.size());
1758
            namedAreaList.addAll(namedAreas);
1759
        }
1760
        if(distributionStatus != null){
1761
            distributionStatusList = new ArrayList<>(distributionStatus.size());
1762
            distributionStatusList.addAll(distributionStatus);
1763
        }
1764

    
1765
        // set default if parameter is null
1766
        if(searchModes == null){
1767
            searchModes = EnumSet.of(TaxaAndNamesSearchMode.doTaxa);
1768
        }
1769

    
1770
        // set sort order and thus override any sort orders which may have been
1771
        // defined by prepare*Search methods
1772
        if(orderHints == null){
1773
            orderHints = OrderHint.NOMENCLATURAL_SORT_ORDER.asList();
1774
        }
1775
        SortField[] sortFields = new SortField[orderHints.size()];
1776
        int i = 0;
1777
        for(OrderHint oh : orderHints){
1778
            sortFields[i++] = oh.toSortField();
1779
        }
1780
//        SortField[] sortFields = new SortField[]{SortField.FIELD_SCORE, new SortField("id", SortField.STRING, false)};
1781
//        SortField[] sortFields = new SortField[]{new SortField(NomenclaturalSortOrderBrigde.NAME_SORT_FIELD_NAME, SortField.STRING, false)};
1782

    
1783

    
1784
        boolean addDistributionFilter = namedAreas != null && namedAreas.size() > 0;
1785

    
1786
        List<LuceneSearch> luceneSearches = new ArrayList<>();
1787
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1788

    
1789
        /*
1790
          ======== filtering by distribution , HOWTO ========
1791

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

    
1797

    
1798
          3. how does it work in spatial?
1799
          see
1800
           - http://www.nsshutdown.com/projects/lucene/whitepaper/locallucene_v2.html
1801
           - http://www.infoq.com/articles/LuceneSpatialSupport
1802
           - http://www.mhaller.de/archives/156-Spatial-search-with-Lucene.html
1803
          ------------------------------------------------------------------------
1804

    
1805
          filter strategies:
1806
          A) use a separate distribution filter per index sub-query/search:
1807
           - byTaxonSyonym (query TaxaonBase):
1808
               use a join area filter (Distribution -> TaxonBase)
1809
           - byCommonName (query DescriptionElementBase): use an area filter on
1810
               DescriptionElementBase !!! PROBLEM !!!
1811
               This cannot work since the distributions are different entities than the
1812
               common names and thus these are different lucene documents.
1813
           - byMisaplliedNames (join query TaxonRelationship -> TaxonBase):
1814
               use a join area filter (Distribution -> TaxonBase)
1815

    
1816
          B) use a common distribution filter for all index sub-query/searches:
1817
           - use a common join area filter (Distribution -> TaxonBase)
1818
           - also implement the byCommonName as join query (CommonName -> TaxonBase)
1819
           PROBLEM in this case: we are losing the fragment highlighting for the
1820
           common names, since the returned documents are always TaxonBases
1821
        */
1822

    
1823
        /* The QueryFactory for creating filter queries on Distributions should
1824
         * The query factory used for the common names query cannot be reused
1825
         * for this case, since we want to only record the text fields which are
1826
         * actually used in the primary query
1827
         */
1828
        QueryFactory distributionFilterQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Distribution.class);
1829

    
1830
        Builder multiIndexByAreaFilterBuilder = new Builder();
1831
        boolean includeUnpublished = searchModes.contains(TaxaAndNamesSearchMode.includeUnpublished);
1832

    
1833
        // search for taxa or synonyms
1834
        if(searchModes.contains(TaxaAndNamesSearchMode.doTaxa) || searchModes.contains(TaxaAndNamesSearchMode.doSynonyms) ) {
1835
            @SuppressWarnings("rawtypes")
1836
            Class<? extends TaxonBase> taxonBaseSubclass = TaxonBase.class;
1837
            String className = null;
1838
            if(searchModes.contains(TaxaAndNamesSearchMode.doTaxa) && !searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
1839
                taxonBaseSubclass = Taxon.class;
1840
            } else if (!searchModes.contains(TaxaAndNamesSearchMode.doTaxa) && searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)) {
1841
                className = "eu.etaxonomy.cdm.model.taxon.Synonym";
1842
            }
1843
            luceneSearches.add(prepareFindByFullTextSearch(taxonBaseSubclass,
1844
                    queryString, classification, subtree, className,
1845
                    includeUnpublished, languages, highlightFragments, sortFields));
1846
            idFieldMap.put(CdmBaseType.TAXON, "id");
1847
            /* A) does not work!!!!
1848
            if(addDistributionFilter){
1849
                // in this case we need a filter which uses a join query
1850
                // to get the TaxonBase documents for the DescriptionElementBase documents
1851
                // which are matching the areas in question
1852
                Query taxonAreaJoinQuery = createByDistributionJoinQuery(
1853
                        namedAreaList,
1854
                        distributionStatusList,
1855
                        distributionFilterQueryFactory
1856
                        );
1857
                multiIndexByAreaFilter.add(new QueryWrapperFilter(taxonAreaJoinQuery), Occur.SHOULD);
1858
            }
1859
            */
1860
            if(addDistributionFilter && searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
1861
                // add additional area filter for synonyms
1862
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1863
                String toField = "accTaxon" + AcceptedTaxonBridge.DOC_KEY_ID_SUFFIX; // id in TaxonBase index
1864

    
1865
                //TODO replace by createByDistributionJoinQuery
1866
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
1867
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class, fromField, true, byDistributionQuery, toField, Taxon.class, ScoreMode.None);
1868
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1869

    
1870
            }
1871
        }
1872

    
1873
        // search by CommonTaxonName
1874
        if(searchModes.contains(TaxaAndNamesSearchMode.doTaxaByCommonNames)) {
1875
            // B)
1876
            QueryFactory descriptionElementQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(DescriptionElementBase.class);
1877
            Query byCommonNameJoinQuery = descriptionElementQueryFactory.newJoinQuery(
1878
                    CommonTaxonName.class,
1879
                    "inDescription.taxon.id",
1880
                    true,
1881
                    QueryFactory.addTypeRestriction(
1882
                                createByDescriptionElementFullTextQuery(queryString, classification, subtree, null, languages, descriptionElementQueryFactory)
1883
                                , CommonTaxonName.class
1884
                                ).build(), "id", null, ScoreMode.Max);
1885
            if (logger.isDebugEnabled()){logger.debug("byCommonNameJoinQuery: " + byCommonNameJoinQuery.toString());}
1886
            LuceneSearch byCommonNameSearch = new LuceneSearch(luceneIndexToolProvider,
1887
                    GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
1888
            byCommonNameSearch.setCdmTypRestriction(Taxon.class);
1889
            Builder builder = new BooleanQuery.Builder();
1890
            builder.add(byCommonNameJoinQuery, Occur.MUST);
1891
            if(!includeUnpublished)  {
1892
                QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1893
                builder.add(taxonBaseQueryFactory.newBooleanQuery("publish", true), Occur.MUST);
1894
            }
1895
            byCommonNameSearch.setQuery(builder.build());
1896
            byCommonNameSearch.setSortFields(sortFields);
1897

    
1898
            idFieldMap.put(CdmBaseType.TAXON, "id");
1899

    
1900
            luceneSearches.add(byCommonNameSearch);
1901

    
1902
            /* A) does not work!!!!
1903
            luceneSearches.add(
1904
                    prepareByDescriptionElementFullTextSearch(CommonTaxonName.class,
1905
                            queryString, classification, null, languages, highlightFragments)
1906
                        );
1907
            idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
1908
            if(addDistributionFilter){
1909
                // in this case we are able to use DescriptionElementBase documents
1910
                // which are matching the areas in question directly
1911
                BooleanQuery byDistributionQuery = createByDistributionQuery(
1912
                        namedAreaList,
1913
                        distributionStatusList,
1914
                        distributionFilterQueryFactory
1915
                        );
1916
                multiIndexByAreaFilter.add(new QueryWrapperFilter(byDistributionQuery), Occur.SHOULD);
1917
            } */
1918
        }
1919

    
1920

    
1921
        // search by misapplied names
1922
        //TODO merge with pro parte synonym search once #7487 is fixed
1923
        if(searchModes.contains(TaxaAndNamesSearchMode.doMisappliedNames) /*|| searchModes.contains(TaxaAndNamesSearchMode.doSynonyms) */) {
1924
            // NOTE:
1925
            // prepareFindByTaxonRelationFullTextSearch() is making use of JoinUtil.createJoinQuery()
1926
            // which allows doing query time joins
1927
            // finds the misapplied name (Taxon B) which is an misapplication for
1928
            // a related Taxon A.
1929
            //
1930
            Set<TaxonRelationshipType> relTypes = new HashSet<>();
1931
            if (searchModes.contains(TaxaAndNamesSearchMode.doMisappliedNames)){
1932
                relTypes.addAll(TaxonRelationshipType.allMisappliedNameTypes());
1933
            }
1934
//            if (searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
1935
//                relTypes.addAll(TaxonRelationshipType.allSynonymTypes());
1936
//            }
1937

    
1938
            luceneSearches.add(prepareFindByTaxonRelationFullTextSearch(
1939
                    new TaxonRelationshipEdge(relTypes, Direction.relatedTo),
1940
                    queryString, classification, subtree, includeUnpublished, languages, highlightFragments, sortFields));
1941
            idFieldMap.put(CdmBaseType.TAXON, "id");
1942

    
1943
            if(addDistributionFilter){
1944
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1945

    
1946
                /*
1947
                 * Here I was facing a weird and nasty bug which took me bugging be really for hours until I found this solution.
1948
                 * Maybe this is a bug in java itself.
1949
                 *
1950
                 * When the string toField is constructed by using the expression TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString()
1951
                 * directly:
1952
                 *
1953
                 *    String toField = "relation." + TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString() +".to.id";
1954
                 *
1955
                 * The byDistributionQuery fails, however when the uuid is first stored in another string variable the query
1956
                 * will execute as expected:
1957
                 *
1958
                 *    String misappliedNameForUuid = TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString();
1959
                 *    String toField = "relation." + misappliedNameForUuid +".to.id";
1960
                 *
1961
                 * Comparing both strings by the String.equals method returns true, so both String are identical.
1962
                 *
1963
                 * The bug occurs when running eu.etaxonomy.cdm.api.service.TaxonServiceSearchTest in eclipse and in maven and seems to to be
1964
                 * 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)
1965
                 * The bug is persistent after a reboot of the development computer.
1966
                 */
1967
//                String misappliedNameForUuid = TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString();
1968
//                String toField = "relation." + misappliedNameForUuid +".to.id";
1969
                String toField = "relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id";
1970
//                System.out.println("relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id".equals("relation." + misappliedNameForUuid +".to.id") ? " > identical" : " > different");
1971
//                System.out.println("relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id".equals("relation." + TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString() +".to.id") ? " > identical" : " > different");
1972

    
1973
                //TODO replace by createByDistributionJoinQuery
1974
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
1975
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class,
1976
                        fromField, true, byDistributionQuery, toField, null, ScoreMode.None);
1977

    
1978
//                debug code for bug described above
1979
                //does not compile anymore since changing from lucene 3.6.2 to lucene 4.10+
1980
//                DocIdSet filterMatchSet = filter.getDocIdSet(luceneIndexToolProvider.getIndexReaderFor(Taxon.class));
1981
//                System.err.println(DocIdBitSetPrinter.docsAsString(filterMatchSet, 100));
1982

    
1983
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1984
            }
1985
        }
1986

    
1987

    
1988
        // search by pro parte synonyms
1989
        if(searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)) {
1990
            //TODO merge with misapplied name search once #7487 is fixed
1991
            Set<TaxonRelationshipType> relTypes = new HashSet<>();
1992
            relTypes.addAll(TaxonRelationshipType.allSynonymTypes());
1993

    
1994
            luceneSearches.add(prepareFindByTaxonRelationFullTextSearch(
1995
                    new TaxonRelationshipEdge(relTypes, Direction.relatedTo),
1996
                    queryString, classification, subtree, includeUnpublished, languages, highlightFragments, sortFields));
1997
            idFieldMap.put(CdmBaseType.TAXON, "id");
1998

    
1999
            if(addDistributionFilter){
2000
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
2001
                String toField = "relation.8a896603-0fa3-44c6-9cd7-df2d8792e577.to.id";
2002
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
2003
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class,
2004
                        fromField, true, byDistributionQuery, toField, null, ScoreMode.None);
2005
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
2006
            }
2007
        }//end pro parte synonyms
2008

    
2009

    
2010

    
2011
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider,
2012
                luceneSearches.toArray(new LuceneSearch[luceneSearches.size()]));
2013

    
2014

    
2015
        if(addDistributionFilter){
2016

    
2017
            // B)
2018
            // in this case we need a filter which uses a join query
2019
            // to get the TaxonBase documents for the DescriptionElementBase documents
2020
            // which are matching the areas in question
2021
            //
2022
            // for doTaxa, doByCommonName
2023
            Query taxonAreaJoinQuery = createByDistributionJoinQuery(
2024
                    namedAreaList,
2025
                    distributionStatusList,
2026
                    distributionFilterQueryFactory,
2027
                    Taxon.class, true
2028
                    );
2029
            multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
2030
        }
2031

    
2032
        if (addDistributionFilter){
2033
            multiSearch.setFilter(multiIndexByAreaFilterBuilder.build());
2034
        }
2035

    
2036

    
2037
        // --- execute search
2038
        TopGroups<BytesRef> topDocsResultSet;
2039
        try {
2040
            topDocsResultSet = multiSearch.executeSearch(pageSize, pageNumber);
2041
        } catch (ParseException e) {
2042
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2043
            luceneParseException.setStackTrace(e.getStackTrace());
2044
            throw luceneParseException;
2045
        }
2046

    
2047
        // --- initialize taxa, highlight matches ....
2048
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
2049

    
2050

    
2051
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2052
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2053

    
2054
        long totalHits = (topDocsResultSet != null) ? Long.valueOf(topDocsResultSet.totalGroupCount) : 0;
2055
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
2056
    }
2057

    
2058
    /**
2059
     * @param namedAreaList at least one area must be in the list
2060
     * @param distributionStatusList optional
2061
     * @param toType toType
2062
     *      Optional parameter. Only used for debugging to print the toType documents
2063
     * @param asFilter TODO
2064
     * @return
2065
     * @throws IOException
2066
     */
2067
    protected Query createByDistributionJoinQuery(
2068
            List<NamedArea> namedAreaList,
2069
            List<PresenceAbsenceTerm> distributionStatusList,
2070
            QueryFactory queryFactory, Class<? extends CdmBase> toType, boolean asFilter
2071
            ) throws IOException {
2072

    
2073
        String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
2074
        String toField = "id"; // id in toType usually this is the TaxonBase index
2075

    
2076
        BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, queryFactory);
2077

    
2078
        ScoreMode scoreMode = asFilter ?  ScoreMode.None : ScoreMode.Max;
2079

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

    
2082
        return taxonAreaJoinQuery;
2083
    }
2084

    
2085
    /**
2086
     * @param namedAreaList
2087
     * @param distributionStatusList
2088
     * @param queryFactory
2089
     * @return
2090
     */
2091
    private BooleanQuery createByDistributionQuery(List<NamedArea> namedAreaList,
2092
            List<PresenceAbsenceTerm> distributionStatusList, QueryFactory queryFactory) {
2093
        Builder areaQueryBuilder = new Builder();
2094
        // area field from Distribution
2095
        areaQueryBuilder.add(queryFactory.newEntityIdsQuery("area.id", namedAreaList), Occur.MUST);
2096

    
2097
        // status field from Distribution
2098
        if(distributionStatusList != null && distributionStatusList.size() > 0){
2099
            areaQueryBuilder.add(queryFactory.newEntityIdsQuery("status.id", distributionStatusList), Occur.MUST);
2100
        }
2101

    
2102
        BooleanQuery areaQuery = areaQueryBuilder.build();
2103
        logger.debug("createByDistributionQuery() query: " + areaQuery.toString());
2104
        return areaQuery;
2105
    }
2106

    
2107
    /**
2108
     * This method has been primarily created for testing the area join query but might
2109
     * also be useful in other situations
2110
     *
2111
     * @param namedAreaList
2112
     * @param distributionStatusList
2113
     * @param classification
2114
     * @param highlightFragments
2115
     * @return
2116
     * @throws IOException
2117
     */
2118
    protected LuceneSearch prepareByDistributionSearch(
2119
            List<NamedArea> namedAreaList, List<PresenceAbsenceTerm> distributionStatusList,
2120
            Classification classification, TaxonNode subtree) throws IOException {
2121

    
2122
        Builder finalQueryBuilder = new Builder();
2123

    
2124
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
2125

    
2126
        // FIXME is this query factory using the wrong type?
2127
        QueryFactory taxonQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Taxon.class);
2128

    
2129
        SortField[] sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING, false)};
2130
        luceneSearch.setSortFields(sortFields);
2131

    
2132

    
2133
        Query byAreaQuery = createByDistributionJoinQuery(namedAreaList, distributionStatusList, taxonQueryFactory, null, false);
2134

    
2135
        finalQueryBuilder.add(byAreaQuery, Occur.MUST);
2136

    
2137
        if(classification != null){
2138
            finalQueryBuilder.add(taxonQueryFactory.newEntityIdQuery(AcceptedTaxonBridge.DOC_KEY_CLASSIFICATION_ID, classification), Occur.MUST);
2139
        }
2140
        if(subtree != null){
2141
            finalQueryBuilder.add(taxonQueryFactory.newTermQuery(AcceptedTaxonBridge.DOC_KEY_TREEINDEX, subtree.treeIndexWc(), true), Occur.MUST);
2142
        }
2143
        BooleanQuery finalQuery = finalQueryBuilder.build();
2144
        logger.info("prepareByAreaSearch() query: " + finalQuery.toString());
2145
        luceneSearch.setQuery(finalQuery);
2146

    
2147
        return luceneSearch;
2148
    }
2149

    
2150
    @Override
2151
    public Pager<SearchResult<TaxonBase>> findByDescriptionElementFullText(
2152
            Class<? extends DescriptionElementBase> clazz, String queryString,
2153
            Classification classification, TaxonNode subtree, List<Feature> features, List<Language> languages,
2154
            boolean highlightFragments, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
2155

    
2156

    
2157
        LuceneSearch luceneSearch = prepareByDescriptionElementFullTextSearch(clazz, queryString, classification, subtree, features, languages, highlightFragments);
2158

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

    
2169
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
2170
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2171

    
2172
        // --- initialize taxa, highlight matches ....
2173
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
2174
        @SuppressWarnings("rawtypes")
2175
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2176
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2177

    
2178
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
2179
        return new DefaultPagerImpl<>(pageNumber, Long.valueOf(totalHits), pageSize, searchResults);
2180

    
2181
    }
2182

    
2183

    
2184
    @Override
2185
    public Pager<SearchResult<TaxonBase>> findByEverythingFullText(String queryString,
2186
            Classification classification, TaxonNode subtree, boolean includeUnpublished, List<Language> languages, boolean highlightFragments,
2187
            Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException, LuceneMultiSearchException {
2188

    
2189
        LuceneSearch luceneSearchByDescriptionElement = prepareByDescriptionElementFullTextSearch(null, queryString,
2190
                classification, subtree,
2191
                null, languages, highlightFragments);
2192
        LuceneSearch luceneSearchByTaxonBase = prepareFindByFullTextSearch(null, queryString, classification, subtree, null,
2193
                includeUnpublished, languages, highlightFragments, null);
2194

    
2195
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider, luceneSearchByDescriptionElement, luceneSearchByTaxonBase);
2196

    
2197
        // --- execute search
2198
        TopGroups<BytesRef> topDocsResultSet;
2199
        try {
2200
            topDocsResultSet = multiSearch.executeSearch(pageSize, pageNumber);
2201
        } catch (ParseException e) {
2202
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2203
            luceneParseException.setStackTrace(e.getStackTrace());
2204
            throw luceneParseException;
2205
        }
2206

    
2207
        // --- initialize taxa, highlight matches ....
2208
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
2209

    
2210
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
2211
        idFieldMap.put(CdmBaseType.TAXON, "id");
2212
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2213

    
2214
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2215
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2216

    
2217
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
2218
        return new DefaultPagerImpl<>(pageNumber, Long.valueOf(totalHits), pageSize, searchResults);
2219

    
2220
    }
2221

    
2222

    
2223
    /**
2224
     * @param clazz
2225
     * @param queryString
2226
     * @param classification
2227
     * @param features
2228
     * @param languages
2229
     * @param highlightFragments
2230
     * @param directorySelectClass
2231
     * @return
2232
     */
2233
    protected LuceneSearch prepareByDescriptionElementFullTextSearch(Class<? extends CdmBase> clazz,
2234
            String queryString, Classification classification, TaxonNode subtree, List<Feature> features,
2235
            List<Language> languages, boolean highlightFragments) {
2236

    
2237
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, DescriptionElementBase.class);
2238
        QueryFactory descriptionElementQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(DescriptionElementBase.class);
2239

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

    
2242
        BooleanQuery finalQuery = createByDescriptionElementFullTextQuery(queryString, classification, subtree, features,
2243
                languages, descriptionElementQueryFactory);
2244

    
2245
        luceneSearch.setSortFields(sortFields);
2246
        luceneSearch.setCdmTypRestriction(clazz);
2247
        luceneSearch.setQuery(finalQuery);
2248
        if(highlightFragments){
2249
            luceneSearch.setHighlightFields(descriptionElementQueryFactory.getTextFieldNamesAsArray());
2250
        }
2251

    
2252
        return luceneSearch;
2253
    }
2254

    
2255
    /**
2256
     * @param queryString
2257
     * @param classification
2258
     * @param features
2259
     * @param languages
2260
     * @param descriptionElementQueryFactory
2261
     * @return
2262
     */
2263
    private BooleanQuery createByDescriptionElementFullTextQuery(String queryString,
2264
            Classification classification, TaxonNode subtree, List<Feature> features,
2265
            List<Language> languages, QueryFactory descriptionElementQueryFactory) {
2266

    
2267
        Builder finalQueryBuilder = new Builder();
2268
        Builder textQueryBuilder = new Builder();
2269

    
2270
        if(!StringUtils.isEmpty(queryString)){
2271

    
2272
            textQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
2273

    
2274
            // common name
2275
            Builder nameQueryBuilder = new Builder();
2276
            if(languages == null || languages.size() == 0){
2277
                nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2278
            } else {
2279
                Builder languageSubQueryBuilder = new Builder();
2280
                for(Language lang : languages){
2281
                    languageSubQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("language.uuid",  lang.getUuid().toString(), false), Occur.SHOULD);
2282
                }
2283
                nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2284
                nameQueryBuilder.add(languageSubQueryBuilder.build(), Occur.MUST);
2285
            }
2286
            textQueryBuilder.add(nameQueryBuilder.build(), Occur.SHOULD);
2287

    
2288

    
2289
            // text field from TextData
2290
            textQueryBuilder.add(descriptionElementQueryFactory.newMultilanguageTextQuery("text", queryString, languages), Occur.SHOULD);
2291

    
2292
            // --- TermBase fields - by representation ----
2293
            // state field from CategoricalData
2294
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.state", queryString, languages), Occur.SHOULD);
2295

    
2296
            // state field from CategoricalData
2297
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.modifyingText", queryString, languages), Occur.SHOULD);
2298

    
2299
            // area field from Distribution
2300
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("area", queryString, languages), Occur.SHOULD);
2301

    
2302
            // status field from Distribution
2303
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("status", queryString, languages), Occur.SHOULD);
2304

    
2305
            finalQueryBuilder.add(textQueryBuilder.build(), Occur.MUST);
2306

    
2307
        }
2308
        // --- classification ----
2309

    
2310

    
2311
        if(classification != null){
2312
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityIdQuery("inDescription.taxon.taxonNodes.classification.id", classification), Occur.MUST);
2313
        }
2314
        if(subtree != null){
2315
            finalQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("inDescription.taxon.taxonNodes.treeIndex", subtree.treeIndexWc(), true), Occur.MUST);
2316
        }
2317

    
2318
        // --- IdentifieableEntity fields - by uuid
2319
        if(features != null && features.size() > 0 ){
2320
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityUuidsQuery("feature.uuid", features), Occur.MUST);
2321
        }
2322

    
2323
        // the description must be associated with a taxon
2324
        finalQueryBuilder.add(descriptionElementQueryFactory.newIsNotNullQuery("inDescription.taxon.id"), Occur.MUST);
2325

    
2326
        BooleanQuery finalQuery = finalQueryBuilder.build();
2327
        logger.info("prepareByDescriptionElementFullTextSearch() query: " + finalQuery.toString());
2328
        return finalQuery;
2329
    }
2330

    
2331
    @Override
2332
    public List<Synonym> createInferredSynonyms(Taxon taxon, Classification classification, SynonymType type, boolean doWithMisappliedNames){
2333

    
2334

    
2335
        List <Synonym> inferredSynonyms = new ArrayList<>();
2336
        List<Synonym> inferredSynonymsToBeRemoved = new ArrayList<>();
2337

    
2338
        Map <UUID, IZoologicalName> zooHashMap = new HashMap<>();
2339
        boolean includeUnpublished = INCLUDE_UNPUBLISHED;
2340

    
2341
        UUID nameUuid= taxon.getName().getUuid();
2342
        IZoologicalName taxonName = getZoologicalName(nameUuid, zooHashMap);
2343
        String epithetOfTaxon = null;
2344
        String infragenericEpithetOfTaxon = null;
2345
        String infraspecificEpithetOfTaxon = null;
2346
        if (taxonName.isSpecies()){
2347
             epithetOfTaxon= taxonName.getSpecificEpithet();
2348
        } else if (taxonName.isInfraGeneric()){
2349
            infragenericEpithetOfTaxon = taxonName.getInfraGenericEpithet();
2350
        } else if (taxonName.isInfraSpecific()){
2351
            infraspecificEpithetOfTaxon = taxonName.getInfraSpecificEpithet();
2352
        }
2353
        String genusOfTaxon = taxonName.getGenusOrUninomial();
2354
        Set<TaxonNode> nodes = taxon.getTaxonNodes();
2355
        List<String> taxonNames = new ArrayList<>();
2356

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

    
2361
            if (node.getClassification().equals(classification)){
2362
                if (!node.isTopmostNode()){
2363
                    TaxonNode parent = node.getParent();
2364
                    parent = CdmBase.deproxy(parent);
2365
                    TaxonName parentName =  parent.getTaxon().getName();
2366
                    IZoologicalName zooParentName = CdmBase.deproxy(parentName);
2367
                    Taxon parentTaxon = CdmBase.deproxy(parent.getTaxon());
2368

    
2369
                    //create inferred synonyms for species, subspecies
2370
                    if ((parentName.isGenus() || parentName.isSpecies() || parentName.getRank().equals(Rank.SUBGENUS())) ){
2371

    
2372
                        Synonym inferredEpithet = null;
2373
                        Synonym inferredGenus = null;
2374
                        Synonym potentialCombination = null;
2375

    
2376
                        List<String> propertyPaths = new ArrayList<>();
2377
                        propertyPaths.add("synonym");
2378
                        propertyPaths.add("synonym.name");
2379
                        List<OrderHint> orderHintsSynonyms = new ArrayList<>();
2380
                        orderHintsSynonyms.add(new OrderHint("titleCache", SortOrder.ASCENDING));
2381

    
2382
                        List<Synonym> synonyMsOfParent = dao.getSynonyms(parentTaxon, SynonymType.HETEROTYPIC_SYNONYM_OF(), null, null,orderHintsSynonyms,propertyPaths);
2383
                        List<Synonym> synonymsOfTaxon= dao.getSynonyms(taxon, SynonymType.HETEROTYPIC_SYNONYM_OF(),
2384
                                null, null,orderHintsSynonyms,propertyPaths);
2385

    
2386
                        List<TaxonRelationship> taxonRelListParent = new ArrayList<>();
2387
                        List<TaxonRelationship> taxonRelListTaxon = new ArrayList<>();
2388
                        if (doWithMisappliedNames){
2389
                            List<OrderHint> orderHintsMisapplied = new ArrayList<>();
2390
                            orderHintsMisapplied.add(new OrderHint("relatedFrom.titleCache", SortOrder.ASCENDING));
2391
                            taxonRelListParent = dao.getTaxonRelationships(parentTaxon, TaxonRelationshipType.MISAPPLIED_NAME_FOR(),
2392
                                    includeUnpublished, null, null, orderHintsMisapplied, propertyPaths, Direction.relatedTo);
2393
                            taxonRelListTaxon = dao.getTaxonRelationships(taxon, TaxonRelationshipType.MISAPPLIED_NAME_FOR(),
2394
                                    includeUnpublished, null, null, orderHintsMisapplied, propertyPaths, Direction.relatedTo);
2395
                        }
2396

    
2397
                        if (type.equals(SynonymType.INFERRED_EPITHET_OF())){
2398
                            for (Synonym synonymRelationOfParent:synonyMsOfParent){
2399

    
2400
                                inferredEpithet = createInferredEpithets(taxon,
2401
                                        zooHashMap, taxonName, epithetOfTaxon,
2402
                                        infragenericEpithetOfTaxon,
2403
                                        infraspecificEpithetOfTaxon,
2404
                                        taxonNames, parentName,
2405
                                        synonymRelationOfParent);
2406

    
2407
                                inferredSynonyms.add(inferredEpithet);
2408
                                zooHashMap.put(inferredEpithet.getName().getUuid(), inferredEpithet.getName());
2409
                                taxonNames.add(inferredEpithet.getName().getNameCache());
2410
                            }
2411

    
2412
                            if (doWithMisappliedNames){
2413

    
2414
                                for (TaxonRelationship taxonRelationship: taxonRelListParent){
2415
                                     Taxon misappliedName = taxonRelationship.getFromTaxon();
2416

    
2417
                                     inferredEpithet = createInferredEpithets(taxon,
2418
                                             zooHashMap, taxonName, epithetOfTaxon,
2419
                                             infragenericEpithetOfTaxon,
2420
                                             infraspecificEpithetOfTaxon,
2421
                                             taxonNames, parentName,
2422
                                             misappliedName);
2423

    
2424
                                    inferredSynonyms.add(inferredEpithet);
2425
                                    zooHashMap.put(inferredEpithet.getName().getUuid(), inferredEpithet.getName());
2426
                                    taxonNames.add(inferredEpithet.getName().getNameCache());
2427
                                }
2428
                            }
2429

    
2430
                            if (!taxonNames.isEmpty()){
2431
                            List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2432
                            IZoologicalName name;
2433
                            if (!synNotInCDM.isEmpty()){
2434
                                inferredSynonymsToBeRemoved.clear();
2435

    
2436
                                for (Synonym syn :inferredSynonyms){
2437
                                    name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2438
                                    if (!synNotInCDM.contains(name.getNameCache())){
2439
                                        inferredSynonymsToBeRemoved.add(syn);
2440
                                    }
2441
                                }
2442

    
2443
                                // Remove identified Synonyms from inferredSynonyms
2444
                                for (Synonym synonym : inferredSynonymsToBeRemoved) {
2445
                                    inferredSynonyms.remove(synonym);
2446
                                }
2447
                            }
2448
                        }
2449

    
2450
                    }else if (type.equals(SynonymType.INFERRED_GENUS_OF())){
2451

    
2452
                        for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2453

    
2454
                            inferredGenus = createInferredGenus(taxon,
2455
                                    zooHashMap, taxonName, epithetOfTaxon,
2456
                                    genusOfTaxon, taxonNames, zooParentName, synonymRelationOfTaxon);
2457

    
2458
                            inferredSynonyms.add(inferredGenus);
2459
                            zooHashMap.put(inferredGenus.getName().getUuid(), inferredGenus.getName());
2460
                            taxonNames.add(inferredGenus.getName().getNameCache());
2461
                        }
2462

    
2463
                        if (doWithMisappliedNames){
2464

    
2465
                            for (TaxonRelationship taxonRelationship: taxonRelListTaxon){
2466
                                Taxon misappliedName = taxonRelationship.getFromTaxon();
2467
                                inferredGenus = createInferredGenus(taxon, zooHashMap, taxonName, infraspecificEpithetOfTaxon, genusOfTaxon, taxonNames, zooParentName,  misappliedName);
2468

    
2469
                                inferredSynonyms.add(inferredGenus);
2470
                                zooHashMap.put(inferredGenus.getName().getUuid(), inferredGenus.getName());
2471
                                 taxonNames.add(inferredGenus.getName().getNameCache());
2472
                            }
2473
                        }
2474

    
2475

    
2476
                        if (!taxonNames.isEmpty()){
2477
                            List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2478
                            IZoologicalName name;
2479
                            if (!synNotInCDM.isEmpty()){
2480
                                inferredSynonymsToBeRemoved.clear();
2481

    
2482
                                for (Synonym syn :inferredSynonyms){
2483
                                    name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2484
                                    if (!synNotInCDM.contains(name.getNameCache())){
2485
                                        inferredSynonymsToBeRemoved.add(syn);
2486
                                    }
2487
                                }
2488

    
2489
                                // Remove identified Synonyms from inferredSynonyms
2490
                                for (Synonym synonym : inferredSynonymsToBeRemoved) {
2491
                                    inferredSynonyms.remove(synonym);
2492
                                }
2493
                            }
2494
                        }
2495

    
2496
                    }else if (type.equals(SynonymType.POTENTIAL_COMBINATION_OF())){
2497

    
2498
                        Reference sourceReference = null; // TODO: Determination of sourceReference is redundant
2499
                        //for all synonyms of the parent...
2500
                        for (Synonym synonymRelationOfParent:synonyMsOfParent){
2501
                            TaxonName synName;
2502
                            HibernateProxyHelper.deproxy(synonymRelationOfParent);
2503

    
2504
                            synName = synonymRelationOfParent.getName();
2505

    
2506
                            // Set the sourceReference
2507
                            sourceReference = synonymRelationOfParent.getSec();
2508

    
2509
                            // Determine the idInSource
2510
                            String idInSourceParent = getIdInSource(synonymRelationOfParent);
2511

    
2512
                            IZoologicalName parentSynZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2513
                            String synParentGenus = parentSynZooName.getGenusOrUninomial();
2514
                            String synParentInfragenericName = null;
2515
                            String synParentSpecificEpithet = null;
2516

    
2517
                            if (parentSynZooName.isInfraGeneric()){
2518
                                synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2519
                            }
2520
                            if (parentSynZooName.isSpecies()){
2521
                                synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2522
                            }
2523

    
2524
                           /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2525
                                synonymsGenus.put(synGenusName, idInSource);
2526
                            }*/
2527

    
2528
                            //for all synonyms of the taxon
2529

    
2530
                            for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2531

    
2532
                                IZoologicalName zooSynName = getZoologicalName(synonymRelationOfTaxon.getName().getUuid(), zooHashMap);
2533
                                potentialCombination = createPotentialCombination(idInSourceParent, parentSynZooName, zooSynName,
2534
                                        synParentGenus,
2535
                                        synParentInfragenericName,
2536
                                        synParentSpecificEpithet, synonymRelationOfTaxon, zooHashMap);
2537

    
2538
                                taxon.addSynonym(potentialCombination, SynonymType.POTENTIAL_COMBINATION_OF());
2539
                                inferredSynonyms.add(potentialCombination);
2540
                                zooHashMap.put(potentialCombination.getName().getUuid(), potentialCombination.getName());
2541
                                 taxonNames.add(potentialCombination.getName().getNameCache());
2542

    
2543
                            }
2544

    
2545
                        }
2546

    
2547
                        if (doWithMisappliedNames){
2548

    
2549
                            for (TaxonRelationship parentRelationship: taxonRelListParent){
2550

    
2551
                                TaxonName misappliedParentName;
2552

    
2553
                                Taxon misappliedParent = parentRelationship.getFromTaxon();
2554
                                misappliedParentName = misappliedParent.getName();
2555

    
2556
                                HibernateProxyHelper.deproxy(misappliedParent);
2557

    
2558
                                // Set the sourceReference
2559
                                sourceReference = misappliedParent.getSec();
2560

    
2561
                                // Determine the idInSource
2562
                                String idInSourceParent = getIdInSource(misappliedParent);
2563

    
2564
                                IZoologicalName parentSynZooName = getZoologicalName(misappliedParentName.getUuid(), zooHashMap);
2565
                                String synParentGenus = parentSynZooName.getGenusOrUninomial();
2566
                                String synParentInfragenericName = null;
2567
                                String synParentSpecificEpithet = null;
2568

    
2569
                                if (parentSynZooName.isInfraGeneric()){
2570
                                    synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2571
                                }
2572
                                if (parentSynZooName.isSpecies()){
2573
                                    synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2574
                                }
2575

    
2576

    
2577
                                for (TaxonRelationship taxonRelationship: taxonRelListTaxon){
2578
                                    Taxon misappliedName = taxonRelationship.getFromTaxon();
2579
                                    IZoologicalName zooMisappliedName = getZoologicalName(misappliedName.getName().getUuid(), zooHashMap);
2580
                                    potentialCombination = createPotentialCombination(
2581
                                            idInSourceParent, parentSynZooName, zooMisappliedName,
2582
                                            synParentGenus,
2583
                                            synParentInfragenericName,
2584
                                            synParentSpecificEpithet, misappliedName, zooHashMap);
2585

    
2586

    
2587
                                    taxon.addSynonym(potentialCombination, SynonymType.POTENTIAL_COMBINATION_OF());
2588
                                    inferredSynonyms.add(potentialCombination);
2589
                                    zooHashMap.put(potentialCombination.getName().getUuid(), potentialCombination.getName());
2590
                                     taxonNames.add(potentialCombination.getName().getNameCache());
2591
                                }
2592
                            }
2593
                        }
2594

    
2595
                        if (!taxonNames.isEmpty()){
2596
                            List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2597
                            IZoologicalName name;
2598
                            if (!synNotInCDM.isEmpty()){
2599
                                inferredSynonymsToBeRemoved.clear();
2600
                                for (Synonym syn :inferredSynonyms){
2601
                                    try{
2602
                                        name = syn.getName();
2603
                                    }catch (ClassCastException e){
2604
                                        name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2605
                                    }
2606
                                    if (!synNotInCDM.contains(name.getNameCache())){
2607
                                        inferredSynonymsToBeRemoved.add(syn);
2608
                                    }
2609
                                 }
2610
                                // Remove identified Synonyms from inferredSynonyms
2611
                                for (Synonym synonym : inferredSynonymsToBeRemoved) {
2612
                                    inferredSynonyms.remove(synonym);
2613
                                }
2614
                            }
2615
                         }
2616
                        }
2617
                    }else {
2618
                        logger.info("The synonym type is not defined.");
2619
                        return inferredSynonyms;
2620
                    }
2621
                }
2622
            }
2623

    
2624
        }
2625

    
2626
        return inferredSynonyms;
2627
    }
2628

    
2629
    private Synonym createPotentialCombination(String idInSourceParent,
2630
            IZoologicalName parentSynZooName, 	IZoologicalName zooSynName, String synParentGenus,
2631
            String synParentInfragenericName, String synParentSpecificEpithet,
2632
            TaxonBase<?> syn, Map<UUID, IZoologicalName> zooHashMap) {
2633
        Synonym potentialCombination;
2634
        Reference sourceReference;
2635
        IZoologicalName inferredSynName;
2636
        HibernateProxyHelper.deproxy(syn);
2637

    
2638
        // Set sourceReference
2639
        sourceReference = syn.getSec();
2640
        if (sourceReference == null){
2641
            logger.warn("The synonym has no sec reference because it is a misapplied name! Take the sec reference of taxon");
2642
            //TODO:Remove
2643
            if (!parentSynZooName.getTaxa().isEmpty()){
2644
                TaxonBase<?> taxon = parentSynZooName.getTaxa().iterator().next();
2645

    
2646
                sourceReference = taxon.getSec();
2647
            }
2648
        }
2649
        String synTaxonSpecificEpithet = zooSynName.getSpecificEpithet();
2650

    
2651
        String synTaxonInfraSpecificName= null;
2652

    
2653
        if (parentSynZooName.isSpecies()){
2654
            synTaxonInfraSpecificName = zooSynName.getInfraSpecificEpithet();
2655
        }
2656

    
2657
        /*if (epithetName != null && !synonymsEpithet.contains(epithetName)){
2658
            synonymsEpithet.add(epithetName);
2659
        }*/
2660

    
2661
        //create potential combinations...
2662
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(syn.getName().getRank());
2663

    
2664
        inferredSynName.setGenusOrUninomial(synParentGenus);
2665
        if (zooSynName.isSpecies()){
2666
              inferredSynName.setSpecificEpithet(synTaxonSpecificEpithet);
2667
              if (parentSynZooName.isInfraGeneric()){
2668
                  inferredSynName.setInfraGenericEpithet(synParentInfragenericName);
2669
              }
2670
        }
2671
        if (zooSynName.isInfraSpecific()){
2672
            inferredSynName.setSpecificEpithet(synParentSpecificEpithet);
2673
            inferredSynName.setInfraSpecificEpithet(synTaxonInfraSpecificName);
2674
        }
2675
        if (parentSynZooName.isInfraGeneric()){
2676
            inferredSynName.setInfraGenericEpithet(synParentInfragenericName);
2677
        }
2678

    
2679

    
2680
        potentialCombination = Synonym.NewInstance(inferredSynName, null);
2681

    
2682
        // Set the sourceReference
2683
        potentialCombination.setSec(sourceReference);
2684

    
2685

    
2686
        // Determine the idInSource
2687
        String idInSourceSyn= getIdInSource(syn);
2688

    
2689
        if (idInSourceParent != null && idInSourceSyn != null) {
2690
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2691
            inferredSynName.addSource(originalSource);
2692
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2693
            potentialCombination.addSource(originalSource);
2694
        }
2695

    
2696
        return potentialCombination;
2697
    }
2698

    
2699
    private Synonym createInferredGenus(Taxon taxon,
2700
            Map<UUID, IZoologicalName> zooHashMap, IZoologicalName taxonName,
2701
            String epithetOfTaxon, String genusOfTaxon,
2702
            List<String> taxonNames, IZoologicalName zooParentName,
2703
            TaxonBase syn) {
2704

    
2705
        Synonym inferredGenus;
2706
        TaxonName synName;
2707
        IZoologicalName inferredSynName;
2708
        synName =syn.getName();
2709
        HibernateProxyHelper.deproxy(syn);
2710

    
2711
        // Determine the idInSource
2712
        String idInSourceSyn = getIdInSource(syn);
2713
        String idInSourceTaxon = getIdInSource(taxon);
2714
        // Determine the sourceReference
2715
        Reference sourceReference = syn.getSec();
2716

    
2717
        //logger.warn(sourceReference.getTitleCache());
2718

    
2719
        synName = syn.getName();
2720
        IZoologicalName synZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2721
        String synSpeciesEpithetName = synZooName.getSpecificEpithet();
2722
                     /* if (synonymsEpithet != null && !synonymsEpithet.contains(synSpeciesEpithetName)){
2723
            synonymsEpithet.add(synSpeciesEpithetName);
2724
        }*/
2725

    
2726
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2727
        //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...
2728

    
2729

    
2730
        inferredSynName.setGenusOrUninomial(genusOfTaxon);
2731
        if (zooParentName.isInfraGeneric()){
2732
            inferredSynName.setInfraGenericEpithet(zooParentName.getInfraGenericEpithet());
2733
        }
2734

    
2735
        if (taxonName.isSpecies()){
2736
            inferredSynName.setSpecificEpithet(synSpeciesEpithetName);
2737
        }
2738
        if (taxonName.isInfraSpecific()){
2739
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2740
            inferredSynName.setInfraSpecificEpithet(synZooName.getInfraGenericEpithet());
2741
        }
2742

    
2743

    
2744
        inferredGenus = Synonym.NewInstance(inferredSynName, null);
2745

    
2746
        // Set the sourceReference
2747
        inferredGenus.setSec(sourceReference);
2748

    
2749
        // Add the original source
2750
        if (idInSourceSyn != null && idInSourceTaxon != null) {
2751
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2752
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2753
            inferredGenus.addSource(originalSource);
2754

    
2755
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2756
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2757
            inferredSynName.addSource(originalSource);
2758
            originalSource = null;
2759

    
2760
        }else{
2761
            logger.error("There is an idInSource missing: " + idInSourceSyn + " of Synonym or " + idInSourceTaxon + " of Taxon");
2762
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2763
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2764
            inferredGenus.addSource(originalSource);
2765

    
2766
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2767
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2768
            inferredSynName.addSource(originalSource);
2769
            originalSource = null;
2770
        }
2771

    
2772
        taxon.addSynonym(inferredGenus, SynonymType.INFERRED_GENUS_OF());
2773

    
2774
        return inferredGenus;
2775
    }
2776

    
2777
    private Synonym createInferredEpithets(Taxon taxon,
2778
            Map<UUID, IZoologicalName> zooHashMap, IZoologicalName taxonName,
2779
            String epithetOfTaxon, String infragenericEpithetOfTaxon,
2780
            String infraspecificEpithetOfTaxon, List<String> taxonNames,
2781
            TaxonName parentName, TaxonBase<?> syn) {
2782

    
2783
        Synonym inferredEpithet;
2784
        TaxonName synName;
2785
        IZoologicalName inferredSynName;
2786
        HibernateProxyHelper.deproxy(syn);
2787

    
2788
        // Determine the idInSource
2789
        String idInSourceSyn = getIdInSource(syn);
2790
        String idInSourceTaxon =  getIdInSource(taxon);
2791
        // Determine the sourceReference
2792
        Reference sourceReference = syn.getSec();
2793

    
2794
        if (sourceReference == null){
2795
             logger.warn("The synonym has no sec reference because it is a misapplied name! Take the sec reference of taxon" + taxon.getSec());
2796
             sourceReference = taxon.getSec();
2797
        }
2798

    
2799
        synName = syn.getName();
2800
        IZoologicalName zooSynName = getZoologicalName(synName.getUuid(), zooHashMap);
2801
        String synGenusName = zooSynName.getGenusOrUninomial();
2802
        String synInfraGenericEpithet = null;
2803
        String synSpecificEpithet = null;
2804

    
2805
        if (zooSynName.getInfraGenericEpithet() != null){
2806
            synInfraGenericEpithet = zooSynName.getInfraGenericEpithet();
2807
        }
2808

    
2809
        if (zooSynName.isInfraSpecific()){
2810
            synSpecificEpithet = zooSynName.getSpecificEpithet();
2811
        }
2812

    
2813
                     /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2814
            synonymsGenus.put(synGenusName, idInSource);
2815
        }*/
2816

    
2817
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2818

    
2819
        // DEBUG TODO: for subgenus or subspecies the infrageneric or infraspecific epithet should be used!!!
2820
        if (epithetOfTaxon == null && infragenericEpithetOfTaxon == null && infraspecificEpithetOfTaxon == null) {
2821
            logger.error("This specificEpithet is NULL" + taxon.getTitleCache());
2822
        }
2823
        inferredSynName.setGenusOrUninomial(synGenusName);
2824

    
2825
        if (parentName.isInfraGeneric()){
2826
            inferredSynName.setInfraGenericEpithet(synInfraGenericEpithet);
2827
        }
2828
        if (taxonName.isSpecies()){
2829
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2830
        }else if (taxonName.isInfraSpecific()){
2831
            inferredSynName.setSpecificEpithet(synSpecificEpithet);
2832
            inferredSynName.setInfraSpecificEpithet(infraspecificEpithetOfTaxon);
2833
        }
2834

    
2835
        inferredEpithet = Synonym.NewInstance(inferredSynName, null);
2836

    
2837
        // Set the sourceReference
2838
        inferredEpithet.setSec(sourceReference);
2839

    
2840
        /* Add the original source
2841
        if (idInSource != null) {
2842
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(idInSource, "InferredEpithetOf", syn.getSec(), null);
2843

    
2844
            // Add the citation
2845
            Reference citation = getCitation(syn);
2846
            if (citation != null) {
2847
                originalSource.setCitation(citation);
2848
                inferredEpithet.addSource(originalSource);
2849
            }
2850
        }*/
2851
        String taxonId = idInSourceTaxon+ "; " + idInSourceSyn;
2852

    
2853

    
2854
        IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2855
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2856

    
2857
        inferredEpithet.addSource(originalSource);
2858

    
2859
        originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2860
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2861

    
2862
        inferredSynName.addSource(originalSource);
2863

    
2864

    
2865

    
2866
        taxon.addSynonym(inferredEpithet, SynonymType.INFERRED_EPITHET_OF());
2867

    
2868
        return inferredEpithet;
2869
    }
2870

    
2871
    /**
2872
     * Returns an existing IZoologicalName or extends an internal hashmap if it does not exist.
2873
     * Very likely only useful for createInferredSynonyms().
2874
     * @param uuid
2875
     * @param zooHashMap
2876
     * @return
2877
     */
2878
    private IZoologicalName getZoologicalName(UUID uuid, Map <UUID, IZoologicalName> zooHashMap) {
2879
        IZoologicalName taxonName =nameDao.findZoologicalNameByUUID(uuid);
2880
        if (taxonName == null) {
2881
            taxonName = zooHashMap.get(uuid);
2882
        }
2883
        return taxonName;
2884
    }
2885

    
2886
    /**
2887
     * Returns the idInSource for a given Synonym.
2888
     * @param syn
2889
     */
2890
    private String getIdInSource(TaxonBase<?> taxonBase) {
2891
        String idInSource = null;
2892
        Set<IdentifiableSource> sources = taxonBase.getSources();
2893
        if (sources.size() == 1) {
2894
            IdentifiableSource source = sources.iterator().next();
2895
            if (source != null) {
2896
                idInSource  = source.getIdInSource();
2897
            }
2898
        } else if (sources.size() > 1) {
2899
            int count = 1;
2900
            idInSource = "";
2901
            for (IdentifiableSource source : sources) {
2902
                idInSource += source.getIdInSource();
2903
                if (count < sources.size()) {
2904
                    idInSource += "; ";
2905
                }
2906
                count++;
2907
            }
2908
        } else if (sources.size() == 0){
2909
            logger.warn("No idInSource for TaxonBase " + taxonBase.getUuid() + " - " + taxonBase.getTitleCache());
2910
        }
2911

    
2912

    
2913
        return idInSource;
2914
    }
2915

    
2916

    
2917
    /**
2918
     * Returns the citation for a given Synonym.
2919
     * @param syn
2920
     */
2921
    private Reference getCitation(Synonym syn) {
2922
        Reference citation = null;
2923
        Set<IdentifiableSource> sources = syn.getSources();
2924
        if (sources.size() == 1) {
2925
            IdentifiableSource source = sources.iterator().next();
2926
            if (source != null) {
2927
                citation = source.getCitation();
2928
            }
2929
        } else if (sources.size() > 1) {
2930
            logger.warn("This Synonym has more than one source: " + syn.getUuid() + " (" + syn.getTitleCache() +")");
2931
        }
2932

    
2933
        return citation;
2934
    }
2935

    
2936
    @Override
2937
    public List<Synonym>  createAllInferredSynonyms(Taxon taxon, Classification tree, boolean doWithMisappliedNames){
2938
        List <Synonym> inferredSynonyms = new ArrayList<>();
2939

    
2940
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.INFERRED_EPITHET_OF(), doWithMisappliedNames));
2941
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.INFERRED_GENUS_OF(), doWithMisappliedNames));
2942
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.POTENTIAL_COMBINATION_OF(), doWithMisappliedNames));
2943

    
2944
        return inferredSynonyms;
2945
    }
2946

    
2947
    @Override
2948
    public List<Classification> listClassifications(TaxonBase taxonBase, Integer limit, Integer start, List<String> propertyPaths) {
2949

    
2950
        // TODO quickly implemented, create according dao !!!!
2951
        Set<TaxonNode> nodes = new HashSet<>();
2952
        Set<Classification> classifications = new HashSet<>();
2953
        List<Classification> list = new ArrayList<>();
2954

    
2955
        if (taxonBase == null) {
2956
            return list;
2957
        }
2958

    
2959
        taxonBase = load(taxonBase.getUuid());
2960

    
2961
        if (taxonBase instanceof Taxon) {
2962
            nodes.addAll(((Taxon)taxonBase).getTaxonNodes());
2963
        } else {
2964
            Taxon taxon = ((Synonym)taxonBase).getAcceptedTaxon();
2965
            if (taxon != null){
2966
                nodes.addAll(taxon.getTaxonNodes());
2967
            }
2968
        }
2969
        for (TaxonNode node : nodes) {
2970
            classifications.add(node.getClassification());
2971
        }
2972
        list.addAll(classifications);
2973
        return list;
2974
    }
2975

    
2976
    @Override
2977
    @Transactional(readOnly = false)
2978
    public UpdateResult changeRelatedTaxonToSynonym(UUID fromTaxonUuid,
2979
            UUID toTaxonUuid,
2980
            TaxonRelationshipType oldRelationshipType,
2981
            SynonymType synonymType) throws DataChangeNoRollbackException {
2982
        UpdateResult result = new UpdateResult();
2983
        Taxon fromTaxon = (Taxon) dao.load(fromTaxonUuid);
2984
        Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
2985
        result = changeRelatedTaxonToSynonym(fromTaxon, toTaxon, oldRelationshipType, synonymType);
2986

    
2987
        result.addUpdatedObject(fromTaxon);
2988
        result.addUpdatedObject(toTaxon);
2989
        result.addUpdatedObject(result.getCdmEntity());
2990

    
2991
        return result;
2992
    }
2993

    
2994
    @Override
2995
    @Transactional(readOnly = false)
2996
    public UpdateResult changeRelatedTaxonToSynonym(Taxon fromTaxon, Taxon toTaxon, TaxonRelationshipType oldRelationshipType,
2997
            SynonymType synonymType) throws DataChangeNoRollbackException {
2998

    
2999
        UpdateResult result = new UpdateResult();
3000
        // Create new synonym using concept name
3001
        TaxonName synonymName = fromTaxon.getName();
3002

    
3003
        // Remove concept relation from taxon
3004
        toTaxon.removeTaxon(fromTaxon, oldRelationshipType);
3005

    
3006
        // Create a new synonym for the taxon
3007
        Synonym synonym;
3008
        if (synonymType != null
3009
                && synonymType.equals(SynonymType.HOMOTYPIC_SYNONYM_OF())){
3010
            synonym = Synonym.NewInstance(synonymName, fromTaxon.getSec());
3011
            toTaxon.addHomotypicSynonym(synonym);
3012
        } else{
3013
            synonym = toTaxon.addHeterotypicSynonymName(synonymName);
3014
        }
3015

    
3016
        this.saveOrUpdate(toTaxon);
3017
        //TODO: configurator and classification
3018
        TaxonDeletionConfigurator config = new TaxonDeletionConfigurator();
3019
        config.setDeleteNameIfPossible(false);
3020
        result.includeResult(this.deleteTaxon(fromTaxon.getUuid(), config, null));
3021
        result.setCdmEntity(synonym);
3022
        result.addUpdatedObject(toTaxon);
3023
        result.addUpdatedObject(synonym);
3024
        return result;
3025
    }
3026

    
3027
    @Override
3028
    public DeleteResult isDeletable(UUID taxonBaseUuid, DeleteConfiguratorBase config){
3029
        DeleteResult result = new DeleteResult();
3030
        TaxonBase<?> taxonBase = load(taxonBaseUuid);
3031
        Set<CdmBase> references = commonService.getReferencingObjectsForDeletion(taxonBase);
3032
        if (taxonBase instanceof Taxon){
3033
            TaxonDeletionConfigurator taxonConfig = (TaxonDeletionConfigurator) config;
3034
            List<String> propertyPaths = new ArrayList();
3035
            propertyPaths.add("taxonNodes");
3036
            Taxon taxon = (Taxon)load(taxonBaseUuid, propertyPaths);
3037

    
3038
            result = isDeletableForTaxon(references, taxonConfig );
3039
        }else{
3040
            SynonymDeletionConfigurator synonymConfig = (SynonymDeletionConfigurator) config;
3041
            result = isDeletableForSynonym(references, synonymConfig);
3042
        }
3043
        return result;
3044
    }
3045

    
3046
    private DeleteResult isDeletableForSynonym(Set<CdmBase> references, SynonymDeletionConfigurator config){
3047
        String message;
3048
        DeleteResult result = new DeleteResult();
3049
        for (CdmBase ref: references){
3050
            if (!(ref instanceof Taxon || ref instanceof TaxonName )){
3051
                message = "The Synonym can't be deleted as long as it is referenced by " + ref.getClass().getSimpleName() + " with id "+ ref.getId();
3052
                result.addException(new ReferencedObjectUndeletableException(message));
3053
                result.addRelatedObject(ref);
3054
                result.setAbort();
3055
            }
3056
        }
3057

    
3058
        return result;
3059
    }
3060

    
3061

    
3062

    
3063
    private DeleteResult isDeletableForTaxon(Set<CdmBase> references, TaxonDeletionConfigurator config){
3064
        String message = null;
3065
        DeleteResult result = new DeleteResult();
3066
        for (CdmBase ref: references){
3067
            if (!(ref instanceof TaxonName)){
3068
            	message = null;
3069
                if (!config.isDeleteSynonymRelations() && (ref instanceof Synonym)){
3070
                    message = "The taxon can't be deleted as long as it has synonyms.";
3071
                }
3072
                if (!config.isDeleteDescriptions() && (ref instanceof DescriptionBase)){
3073
                    message = "The taxon can't be deleted as long as it has factual data.";
3074
                }
3075

    
3076
                if (!config.isDeleteTaxonNodes() && (ref instanceof TaxonNode)){
3077

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

    
3083
                }
3084
                if (!config.isDeleteTaxonRelationships() && (ref instanceof TaxonRelationship)){
3085
                    if (!config.isDeleteMisappliedNamesAndInvalidDesignations() &&
3086
                            (((TaxonRelationship)ref).getType().isMisappliedNameOrInvalidDesignation())){
3087
                        message = "The taxon can't be deleted as long as it has misapplied names or invalid designations.";
3088
                    } else{
3089
                        message = "The taxon can't be deleted as long as it belongs to taxon relationship.";
3090
                    }
3091
                }
3092
                if (ref instanceof PolytomousKeyNode){
3093
                    message = "The taxon can't be deleted as long as it is referenced by a polytomous key node.";
3094
                }
3095

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

    
3100

    
3101
               /* //PolytomousKeyNode
3102
                if (referencingObject.isInstanceOf(PolytomousKeyNode.class)){
3103
                    String message = "Taxon" + taxon.getTitleCache() + " can't be deleted as it is used in polytomous key node";
3104
                    return message;
3105
                }*/
3106

    
3107
                //TaxonInteraction
3108
                if (ref.isInstanceOf(TaxonInteraction.class)){
3109
                    message = "Taxon can't be deleted as it is used in taxonInteraction#taxon2";
3110
                }
3111

    
3112
              //TaxonInteraction
3113
                if (ref.isInstanceOf(DeterminationEvent.class)){
3114
                    message = "Taxon can't be deleted as it is used in a determination event";
3115
                }
3116
            }
3117
            if (message != null){
3118
	            result.addException(new ReferencedObjectUndeletableException(message));
3119
	            result.addRelatedObject(ref);
3120
	            result.setAbort();
3121
            }
3122
        }
3123

    
3124
        return result;
3125
    }
3126

    
3127
    @Override
3128
    public IncludedTaxaDTO listIncludedTaxa(UUID taxonUuid, IncludedTaxonConfiguration config) {
3129
        IncludedTaxaDTO result = new IncludedTaxaDTO(taxonUuid);
3130

    
3131
        //preliminary implementation
3132

    
3133
        Set<Taxon> taxa = new HashSet<>();
3134
        TaxonBase<?> taxonBase = find(taxonUuid);
3135
        if (taxonBase == null){
3136
            return new IncludedTaxaDTO();
3137
        }else if (taxonBase.isInstanceOf(Taxon.class)){
3138
            Taxon taxon = CdmBase.deproxy(taxonBase, Taxon.class);
3139
            taxa.add(taxon);
3140
        }else if (taxonBase.isInstanceOf(Synonym.class)){
3141
            //TODO partial synonyms ??
3142
            //TODO synonyms in general
3143
            Synonym syn = CdmBase.deproxy(taxonBase, Synonym.class);
3144
            taxa.add(syn.getAcceptedTaxon());
3145
        }else{
3146
            throw new IllegalArgumentException("Unhandled class " + taxonBase.getClass().getSimpleName());
3147
        }
3148

    
3149
        Set<Taxon> related = makeRelatedIncluded(taxa, result, config);
3150
        int i = 0;
3151
        while((! related.isEmpty()) && i++ < 100){  //to avoid
3152
             related = makeRelatedIncluded(related, result, config);
3153
        }
3154

    
3155
        return result;
3156
    }
3157

    
3158
    /**
3159
     * @param uncheckedTaxa
3160
     * @param existingTaxa
3161
     * @param config
3162
     *
3163
     * Computes all children and conceptually congruent and included taxa and adds them to the existingTaxa
3164
     * data structure.
3165
     * @return the set of conceptually related taxa for further use
3166
     */
3167
    private Set<Taxon> makeRelatedIncluded(Set<Taxon> uncheckedTaxa, IncludedTaxaDTO existingTaxa, IncludedTaxonConfiguration config) {
3168

    
3169
        //children
3170
        Set<TaxonNode> taxonNodes = new HashSet<>();
3171
        for (Taxon taxon: uncheckedTaxa){
3172
            taxonNodes.addAll(taxon.getTaxonNodes());
3173
        }
3174

    
3175
        Set<Taxon> children = new HashSet<>();
3176
        if (! config.onlyCongruent){
3177
            for (TaxonNode node: taxonNodes){
3178
                List<TaxonNode> childNodes = nodeService.loadChildNodesOfTaxonNode(node, null, true, config.includeUnpublished, null);
3179
                for (TaxonNode child : childNodes){
3180
                    children.add(child.getTaxon());
3181
                }
3182
            }
3183
            children.remove(null);  // just to be on the save side
3184
        }
3185

    
3186
        Iterator<Taxon> it = children.iterator();
3187
        while(it.hasNext()){
3188
            UUID uuid = it.next().getUuid();
3189
            if (existingTaxa.contains(uuid)){
3190
                it.remove();
3191
            }else{
3192
                existingTaxa.addIncludedTaxon(uuid, new ArrayList<>(), false);
3193
            }
3194
        }
3195

    
3196
        //concept relations
3197
        Set<Taxon> uncheckedAndChildren = new HashSet<>(uncheckedTaxa);
3198
        uncheckedAndChildren.addAll(children);
3199

    
3200
        Set<Taxon> relatedTaxa = makeConceptIncludedTaxa(uncheckedAndChildren, existingTaxa, config);
3201

    
3202

    
3203
        Set<Taxon> result = new HashSet<>(relatedTaxa);
3204
        return result;
3205
    }
3206

    
3207
    /**
3208
     * Computes all conceptually congruent or included taxa and adds them to the existingTaxa data structure.
3209
     * @return the set of these computed taxa
3210
     */
3211
    private Set<Taxon> makeConceptIncludedTaxa(Set<Taxon> unchecked, IncludedTaxaDTO existingTaxa, IncludedTaxonConfiguration config) {
3212
        Set<Taxon> result = new HashSet<>();
3213

    
3214
        for (Taxon taxon : unchecked){
3215
            Set<TaxonRelationship> fromRelations = taxon.getRelationsFromThisTaxon();
3216
            Set<TaxonRelationship> toRelations = taxon.getRelationsToThisTaxon();
3217

    
3218
            for (TaxonRelationship fromRel : fromRelations){
3219
                if (config.includeDoubtful == false && fromRel.isDoubtful()){
3220
                    continue;
3221
                }
3222
                TaxonRelationshipType fromRelType = fromRel.getType();
3223
                if (fromRelType.equals(TaxonRelationshipType.CONGRUENT_TO()) ||
3224
                        !config.onlyCongruent && (
3225
                            fromRelType.equals(TaxonRelationshipType.INCLUDES()) ||
3226
                            fromRelType.equals(TaxonRelationshipType.CONGRUENT_OR_INCLUDES())
3227
                        )
3228
                    ){
3229
                    result.add(fromRel.getToTaxon());
3230
                }
3231
            }
3232

    
3233
            for (TaxonRelationship toRel : toRelations){
3234
                if (config.includeDoubtful == false && toRel.isDoubtful()){
3235
                    continue;
3236
                }
3237
                TaxonRelationshipType fromRelType = toRel.getType();
3238
                if (fromRelType.equals(TaxonRelationshipType.CONGRUENT_TO()) ||
3239
                        !config.includeDoubtful && fromRelType.equals(TaxonRelationshipType.TAXONOMICALLY_INCLUDED_IN())){
3240
                    result.add(toRel.getFromTaxon());
3241
                }
3242
            }
3243
        }
3244

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

    
3257
    @Override
3258
    public List<TaxonBase> findTaxaByName(MatchingTaxonConfigurator config){
3259
        List<TaxonBase> taxonList = dao.getTaxaByName(true, config.isIncludeSynonyms(), false, false, false,
3260
                config.getTaxonNameTitle(), null, null, MatchMode.EXACT, null, config.isIncludeSynonyms(), null, 0, 0, config.getPropertyPath());
3261
        return taxonList;
3262
    }
3263

    
3264
	@Override
3265
	@Transactional(readOnly = true)
3266
	public <S extends TaxonBase> Pager<IdentifiedEntityDTO<S>> findByIdentifier(
3267
			Class<S> clazz, String identifier, DefinedTerm identifierType, TaxonNode subtreeFilter,
3268
			MatchMode matchmode, boolean includeEntity, Integer pageSize,
3269
			Integer pageNumber,	List<String> propertyPaths) {
3270
		if (subtreeFilter == null){
3271
			return findByIdentifier(clazz, identifier, identifierType, matchmode, includeEntity, pageSize, pageNumber, propertyPaths);
3272
		}
3273

    
3274
		long numberOfResults = dao.countByIdentifier(clazz, identifier, identifierType, subtreeFilter, matchmode);
3275
        List<Object[]> daoResults = new ArrayList<>();
3276
        if(numberOfResults > 0) { // no point checking again
3277
        	daoResults = dao.findByIdentifier(clazz, identifier, identifierType, subtreeFilter,
3278
    				matchmode, includeEntity, pageSize, pageNumber, propertyPaths);
3279
        }
3280

    
3281
        List<IdentifiedEntityDTO<S>> result = new ArrayList<>();
3282
        for (Object[] daoObj : daoResults){
3283
        	if (includeEntity){
3284
        		result.add(new IdentifiedEntityDTO<>((DefinedTerm)daoObj[0], (String)daoObj[1], (S)daoObj[2]));
3285
        	}else{
3286
        		result.add(new IdentifiedEntityDTO<>((DefinedTerm)daoObj[0], (String)daoObj[1], (UUID)daoObj[2], (String)daoObj[3], null));
3287
        	}
3288
        }
3289
		return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, result);
3290
	}
3291

    
3292
	@Override
3293
    @Transactional(readOnly = true)
3294
    public <S extends TaxonBase> Pager<MarkedEntityDTO<S>> findByMarker(
3295
            Class<S> clazz, MarkerType markerType, Boolean markerValue,
3296
            TaxonNode subtreeFilter, boolean includeEntity, TaxonTitleType titleType,
3297
            Integer pageSize, Integer pageNumber, List<String> propertyPaths) {
3298
        if (subtreeFilter == null){
3299
            return super.findByMarker (clazz, markerType, markerValue, includeEntity, pageSize, pageNumber, propertyPaths);
3300
        }
3301

    
3302
        Long numberOfResults = dao.countByMarker(clazz, markerType, markerValue, subtreeFilter);
3303
        List<Object[]> daoResults = new ArrayList<>();
3304
        if(numberOfResults > 0) { // no point checking again
3305
            daoResults = dao.findByMarker(clazz, markerType, markerValue, subtreeFilter,
3306
                    includeEntity, titleType, pageSize, pageNumber, propertyPaths);
3307
        }
3308

    
3309
        List<MarkedEntityDTO<S>> result = new ArrayList<>();
3310
        for (Object[] daoObj : daoResults){
3311
            if (includeEntity){
3312
                result.add(new MarkedEntityDTO<S>((MarkerType)daoObj[0], (Boolean)daoObj[1], (S)daoObj[2]));
3313
            }else{
3314
                result.add(new MarkedEntityDTO<S>((MarkerType)daoObj[0], (Boolean)daoObj[1], (UUID)daoObj[2], (String)daoObj[3]));
3315
            }
3316
        }
3317
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, result);
3318
    }
3319

    
3320
    @Override
3321
	@Transactional(readOnly = false)
3322
	public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym, UUID newTaxonUUID, boolean moveHomotypicGroup,
3323
            SynonymType newSynonymType, Reference newSecundum, String newSecundumDetail,
3324
            boolean keepSecundumIfUndefined) throws HomotypicalGroupChangeException {
3325

    
3326
	    UpdateResult result = new UpdateResult();
3327
		Taxon newTaxon = CdmBase.deproxy(dao.load(newTaxonUUID),Taxon.class);
3328
		result = moveSynonymToAnotherTaxon(oldSynonym, newTaxon, moveHomotypicGroup, newSynonymType,
3329
		        newSecundum, newSecundumDetail, keepSecundumIfUndefined);
3330

    
3331
		return result;
3332
	}
3333

    
3334
	@Override
3335
	public UpdateResult moveFactualDateToAnotherTaxon(UUID fromTaxonUuid, UUID toTaxonUuid){
3336
		UpdateResult result = new UpdateResult();
3337

    
3338
		Taxon fromTaxon = (Taxon)dao.load(fromTaxonUuid);
3339
		Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
3340
    	for(TaxonDescription description : fromTaxon.getDescriptions()){
3341
              //reload to avoid session conflicts
3342
              description = HibernateProxyHelper.deproxy(description, TaxonDescription.class);
3343

    
3344
              String moveMessage = String.format("Description moved from %s", fromTaxon);
3345
              if(description.isProtectedTitleCache()){
3346
                  String separator = "";
3347
                  if(!StringUtils.isBlank(description.getTitleCache())){
3348
                      separator = " - ";
3349
                  }
3350
                  description.setTitleCache(description.getTitleCache() + separator + moveMessage, true);
3351
              }
3352
              Annotation annotation = Annotation.NewInstance(moveMessage, Language.getDefaultLanguage());
3353
              annotation.setAnnotationType(AnnotationType.TECHNICAL());
3354
              description.addAnnotation(annotation);
3355
              toTaxon.addDescription(description);
3356
              dao.saveOrUpdate(toTaxon);
3357
              dao.saveOrUpdate(fromTaxon);
3358
              result.addUpdatedObject(toTaxon);
3359
              result.addUpdatedObject(fromTaxon);
3360

    
3361
        }
3362

    
3363
    	return result;
3364
	}
3365

    
3366
	@Override
3367
	@Transactional(readOnly = false)
3368
	public UpdateResult swapSynonymAndAcceptedTaxon(UUID synonymUUid,
3369
			UUID acceptedTaxonUuid, boolean setNameInSource) {
3370
		TaxonBase<?> base = this.load(synonymUUid);
3371
		Synonym syn = HibernateProxyHelper.deproxy(base, Synonym.class);
3372
		base = this.load(acceptedTaxonUuid);
3373
		Taxon taxon = HibernateProxyHelper.deproxy(base, Taxon.class);
3374

    
3375
		return this.swapSynonymAndAcceptedTaxon(syn, taxon, setNameInSource);
3376
	}
3377

    
3378
    /**
3379
     * {@inheritDoc}
3380
     */
3381
    @Override
3382
    public TaxonRelationshipsDTO listTaxonRelationships(UUID taxonUuid, Set<TaxonRelationshipType> directTypes,
3383
            Set<TaxonRelationshipType> inversTypes,
3384
            Direction direction, boolean groupMisapplications,
3385
            boolean includeUnpublished,
3386
            Integer pageSize, Integer pageNumber) {
3387
        TaxonBase<?> taxonBase = dao.load(taxonUuid);
3388
        if (taxonBase == null || !taxonBase.isInstanceOf(TaxonBase.class)){
3389
            //TODO handle
3390
            throw new RuntimeException("Taxon for uuid " + taxonUuid + " not found");
3391
        }else{
3392
            Taxon taxon = CdmBase.deproxy(taxonBase, Taxon.class);
3393
            boolean doDirect = (direction == null || direction == Direction.relatedTo);
3394
            boolean doInvers = (direction == null || direction == Direction.relatedFrom);
3395

    
3396
            TaxonRelationshipsDTO dto = new TaxonRelationshipsDTO();
3397

    
3398
            //TODO paging is difficult because misapplication string is an attribute
3399
            //of toplevel dto
3400
//        long numberOfResults = dao.countTaxonRelationships(taxon, type, includeUnpublished, TaxonRelationship.Direction.relatedFrom);
3401
//        List<TaxonRelationshipsDTO> results = new ArrayList<>();
3402
//        if(numberOfResults > 0) { // no point checking again
3403
//            results = dao.getTaxonRelationships(taxon, type, includeUnpublished, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedFrom);
3404
//        }
3405
//
3406
//        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);;
3407

    
3408
            //TODO languages
3409
            List<Language> languages = null;
3410
            if (doDirect){
3411
                direction = Direction.relatedTo;
3412
                //TODO order hints, property path
3413
                List<TaxonRelationship> relations = dao.getTaxonRelationships(taxon, directTypes, includeUnpublished, pageSize, pageNumber, null, null, direction.invers());
3414
                for (TaxonRelationship relation : relations){
3415
                    dto.addRelation(relation, direction, languages);
3416
                }
3417
            }
3418
            if (doInvers){
3419
                direction = Direction.relatedFrom;
3420
                //TODO order hints, property path
3421
                List<TaxonRelationship> relations = dao.getTaxonRelationships(taxon, inversTypes, includeUnpublished, pageSize, pageNumber, null, null, direction.invers());
3422
                for (TaxonRelationship relation : relations){
3423
                    dto.addRelation(relation, direction, languages);
3424
                }
3425
            }
3426
            if (groupMisapplications){
3427
                //TODO
3428
                dto.createMisapplicationString();
3429
            }
3430
            return dto;
3431
        }
3432
    }
3433

    
3434
}
(92-92/100)