Project

General

Profile

Download (147 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.lang.StringUtils;
26
import org.apache.log4j.Logger;
27
import org.apache.lucene.queryparser.classic.ParseException;
28
import org.apache.lucene.sandbox.queries.DuplicateFilter;
29
import org.apache.lucene.search.BooleanClause.Occur;
30
import org.apache.lucene.search.BooleanQuery;
31
import org.apache.lucene.search.BooleanQuery.Builder;
32
import org.apache.lucene.search.Query;
33
import org.apache.lucene.search.SortField;
34
import org.apache.lucene.search.grouping.TopGroups;
35
import org.apache.lucene.search.join.ScoreMode;
36
import org.apache.lucene.util.BytesRef;
37
import org.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.exception.DataChangeNoRollbackException;
52
import eu.etaxonomy.cdm.api.service.exception.HomotypicalGroupChangeException;
53
import eu.etaxonomy.cdm.api.service.exception.ReferencedObjectUndeletableException;
54
import eu.etaxonomy.cdm.api.service.pager.Pager;
55
import eu.etaxonomy.cdm.api.service.pager.impl.DefaultPagerImpl;
56
import eu.etaxonomy.cdm.api.service.search.ILuceneIndexToolProvider;
57
import eu.etaxonomy.cdm.api.service.search.ISearchResultBuilder;
58
import eu.etaxonomy.cdm.api.service.search.LuceneMultiSearch;
59
import eu.etaxonomy.cdm.api.service.search.LuceneMultiSearchException;
60
import eu.etaxonomy.cdm.api.service.search.LuceneParseException;
61
import eu.etaxonomy.cdm.api.service.search.LuceneSearch;
62
import eu.etaxonomy.cdm.api.service.search.QueryFactory;
63
import eu.etaxonomy.cdm.api.service.search.SearchResult;
64
import eu.etaxonomy.cdm.api.service.search.SearchResultBuilder;
65
import eu.etaxonomy.cdm.api.service.util.TaxonRelationshipEdge;
66
import eu.etaxonomy.cdm.common.monitor.IProgressMonitor;
67
import eu.etaxonomy.cdm.hibernate.HibernateProxyHelper;
68
import eu.etaxonomy.cdm.hibernate.search.DefinedTermBaseClassBridge;
69
import eu.etaxonomy.cdm.hibernate.search.GroupByTaxonClassBridge;
70
import eu.etaxonomy.cdm.hibernate.search.MultilanguageTextFieldBridge;
71
import eu.etaxonomy.cdm.model.CdmBaseType;
72
import eu.etaxonomy.cdm.model.common.Annotation;
73
import eu.etaxonomy.cdm.model.common.AnnotationType;
74
import eu.etaxonomy.cdm.model.common.CdmBase;
75
import eu.etaxonomy.cdm.model.common.DefinedTerm;
76
import eu.etaxonomy.cdm.model.common.IdentifiableEntity;
77
import eu.etaxonomy.cdm.model.common.IdentifiableSource;
78
import eu.etaxonomy.cdm.model.common.Language;
79
import eu.etaxonomy.cdm.model.common.MarkerType;
80
import eu.etaxonomy.cdm.model.common.OriginalSourceType;
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.Distribution;
86
import eu.etaxonomy.cdm.model.description.Feature;
87
import eu.etaxonomy.cdm.model.description.IIdentificationKey;
88
import eu.etaxonomy.cdm.model.description.PolytomousKeyNode;
89
import eu.etaxonomy.cdm.model.description.PresenceAbsenceTerm;
90
import eu.etaxonomy.cdm.model.description.SpecimenDescription;
91
import eu.etaxonomy.cdm.model.description.TaxonDescription;
92
import eu.etaxonomy.cdm.model.description.TaxonInteraction;
93
import eu.etaxonomy.cdm.model.description.TaxonNameDescription;
94
import eu.etaxonomy.cdm.model.location.NamedArea;
95
import eu.etaxonomy.cdm.model.media.Media;
96
import eu.etaxonomy.cdm.model.media.MediaRepresentation;
97
import eu.etaxonomy.cdm.model.media.MediaUtils;
98
import eu.etaxonomy.cdm.model.name.HomotypicalGroup;
99
import eu.etaxonomy.cdm.model.name.HybridRelationship;
100
import eu.etaxonomy.cdm.model.name.IZoologicalName;
101
import eu.etaxonomy.cdm.model.name.Rank;
102
import eu.etaxonomy.cdm.model.name.TaxonName;
103
import eu.etaxonomy.cdm.model.name.TaxonNameFactory;
104
import eu.etaxonomy.cdm.model.occurrence.DerivedUnit;
105
import eu.etaxonomy.cdm.model.occurrence.DeterminationEvent;
106
import eu.etaxonomy.cdm.model.occurrence.SpecimenOrObservationBase;
107
import eu.etaxonomy.cdm.model.reference.Reference;
108
import eu.etaxonomy.cdm.model.taxon.Classification;
109
import eu.etaxonomy.cdm.model.taxon.HomotypicGroupTaxonComparator;
110
import eu.etaxonomy.cdm.model.taxon.ITaxonTreeNode;
111
import eu.etaxonomy.cdm.model.taxon.Synonym;
112
import eu.etaxonomy.cdm.model.taxon.SynonymType;
113
import eu.etaxonomy.cdm.model.taxon.Taxon;
114
import eu.etaxonomy.cdm.model.taxon.TaxonBase;
115
import eu.etaxonomy.cdm.model.taxon.TaxonNode;
116
import eu.etaxonomy.cdm.model.taxon.TaxonRelationship;
117
import eu.etaxonomy.cdm.model.taxon.TaxonRelationshipType;
118
import eu.etaxonomy.cdm.persistence.dao.common.ICdmGenericDao;
119
import eu.etaxonomy.cdm.persistence.dao.common.IOrderedTermVocabularyDao;
120
import eu.etaxonomy.cdm.persistence.dao.initializer.AbstractBeanInitializer;
121
import eu.etaxonomy.cdm.persistence.dao.name.ITaxonNameDao;
122
import eu.etaxonomy.cdm.persistence.dao.occurrence.IOccurrenceDao;
123
import eu.etaxonomy.cdm.persistence.dao.taxon.IClassificationDao;
124
import eu.etaxonomy.cdm.persistence.dao.taxon.ITaxonDao;
125
import eu.etaxonomy.cdm.persistence.dao.taxon.ITaxonNodeDao;
126
import eu.etaxonomy.cdm.persistence.dto.UuidAndTitleCache;
127
import eu.etaxonomy.cdm.persistence.query.MatchMode;
128
import eu.etaxonomy.cdm.persistence.query.OrderHint;
129
import eu.etaxonomy.cdm.persistence.query.OrderHint.SortOrder;
130
import eu.etaxonomy.cdm.persistence.query.TaxonTitleType;
131
import eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy;
132

    
133

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

    
143
    public static final String POTENTIAL_COMBINATION_NAMESPACE = "Potential combination";
144

    
145
    public static final String INFERRED_EPITHET_NAMESPACE = "Inferred epithet";
146

    
147
    public static final String INFERRED_GENUS_NAMESPACE = "Inferred genus";
148

    
149
    @Autowired
150
    private ITaxonNodeDao taxonNodeDao;
151

    
152
    @Autowired
153
    private ITaxonNameDao nameDao;
154

    
155
    @Autowired
156
    private INameService nameService;
157

    
158
    @Autowired
159
    private IOccurrenceService occurrenceService;
160

    
161
    @Autowired
162
    private ITaxonNodeService nodeService;
163

    
164
    @Autowired
165
    private ICdmGenericDao genericDao;
166

    
167
    @Autowired
168
    private IDescriptionService descriptionService;
169

    
170
    @Autowired
171
    private IOrderedTermVocabularyDao orderedVocabularyDao;
172

    
173
    @Autowired
174
    private IOccurrenceDao occurrenceDao;
175

    
176
    @Autowired
177
    private IClassificationDao classificationDao;
178

    
179
    @Autowired
180
    private AbstractBeanInitializer beanInitializer;
181

    
182
    @Autowired
183
    private ILuceneIndexToolProvider luceneIndexToolProvider;
184

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

    
192
    /**
193
     * FIXME Candidate for harmonization
194
     * rename searchByName ?
195
     */
196
    @Override
197
    public List<TaxonBase> searchTaxaByName(String name, Reference sec) {
198
        return dao.getTaxaByName(name, sec);
199
    }
200

    
201
    @Override
202
    @Transactional(readOnly = false)
203
    public UpdateResult swapSynonymAndAcceptedTaxon(Synonym synonym, Taxon acceptedTaxon){
204
    	UpdateResult result = new UpdateResult();
205
        TaxonName synonymName = synonym.getName();
206
        synonymName.removeTaxonBase(synonym);
207
        TaxonName taxonName = acceptedTaxon.getName();
208
        taxonName.removeTaxonBase(acceptedTaxon);
209

    
210
        synonym.setName(taxonName);
211
        synonym.setTitleCache(null, false);
212
        synonym.getTitleCache();
213
        acceptedTaxon.setName(synonymName);
214
        acceptedTaxon.setTitleCache(null, false);
215
        acceptedTaxon.getTitleCache();
216
        saveOrUpdate(synonym);
217
        saveOrUpdate(acceptedTaxon);
218
        result.addUpdatedObject(acceptedTaxon);
219
        result.addUpdatedObject(synonym);
220
		return result;
221

    
222
        // the accepted taxon needs a new uuid because the concept has changed
223
        // FIXME this leads to an error "HibernateException: immutable natural identifier of an instance of eu.etaxonomy.cdm.model.taxon.Taxon was altered"
224
        //acceptedTaxon.setUuid(UUID.randomUUID());
225
    }
226

    
227

    
228
    @Override
229
    @Transactional(readOnly = false)
230
    public UpdateResult changeSynonymToAcceptedTaxon(Synonym synonym, Taxon acceptedTaxon, boolean deleteSynonym) {
231
        UpdateResult result = new UpdateResult();
232
        TaxonName acceptedName = acceptedTaxon.getName();
233
        TaxonName synonymName = synonym.getName();
234
        HomotypicalGroup synonymHomotypicGroup = synonymName.getHomotypicalGroup();
235

    
236
        //check synonym is not homotypic
237
        if (acceptedName.getHomotypicalGroup().equals(synonymHomotypicGroup)){
238
            String message = "The accepted taxon and the synonym are part of the same homotypical group and therefore can not be both accepted.";
239
            result.addException(new HomotypicalGroupChangeException(message));
240
            result.setAbort();
241
            return result;
242
        }
243

    
244
        Taxon newAcceptedTaxon = Taxon.NewInstance(synonymName, acceptedTaxon.getSec());
245
        dao.save(newAcceptedTaxon);
246
        result.setCdmEntity(newAcceptedTaxon);
247
        SynonymType relTypeForGroup = SynonymType.HOMOTYPIC_SYNONYM_OF();
248
        List<Synonym> heteroSynonyms = acceptedTaxon.getSynonymsInGroup(synonymHomotypicGroup);
249

    
250
        for (Synonym heteroSynonym : heteroSynonyms){
251
            if (synonym.equals(heteroSynonym)){
252
                acceptedTaxon.removeSynonym(heteroSynonym, false);
253
            }else{
254
                //move synonyms in same homotypic group to new accepted taxon
255
                newAcceptedTaxon.addSynonym(heteroSynonym, relTypeForGroup);
256
            }
257
        }
258
        dao.saveOrUpdate(acceptedTaxon);
259
        result.addUpdatedObject(acceptedTaxon);
260
        if (deleteSynonym){
261

    
262
            try {
263
                this.dao.flush();
264
                SynonymDeletionConfigurator config = new SynonymDeletionConfigurator();
265
                config.setDeleteNameIfPossible(false);
266
                this.deleteSynonym(synonym, config);
267

    
268
            } catch (Exception e) {
269
                result.addException(e);
270
            }
271
        }
272

    
273
        return result;
274
    }
275

    
276
    @Override
277
    @Transactional(readOnly = false)
278
    public UpdateResult changeSynonymToAcceptedTaxon(UUID synonymUuid,
279
            UUID acceptedTaxonUuid,
280
            UUID newParentNodeUuid,
281
            boolean deleteSynonym)  {
282
        UpdateResult result = new UpdateResult();
283
        Synonym synonym = CdmBase.deproxy(dao.load(synonymUuid), Synonym.class);
284
        Taxon acceptedTaxon = CdmBase.deproxy(dao.load(acceptedTaxonUuid), Taxon.class);
285
        result =  changeSynonymToAcceptedTaxon(synonym, acceptedTaxon, deleteSynonym);
286
        Taxon newTaxon = (Taxon)result.getCdmEntity();
287
        TaxonNode newParentNode = taxonNodeDao.load(newParentNodeUuid);
288
        TaxonNode newNode = newParentNode.addChildTaxon(newTaxon, null, null);
289
        taxonNodeDao.save(newNode);
290
        result.addUpdatedObject(newTaxon);
291
        result.addUpdatedObject(acceptedTaxon);
292
        result.setCdmEntity(newNode);
293
        return result;
294
    }
295

    
296
    @Override
297
    @Transactional(readOnly = false)
298
    public UpdateResult createSynonym(UUID acceptedTaxonUuid,
299
            Synonym newSynonym)  {
300
        UpdateResult result = new UpdateResult();
301
        for (HybridRelationship rel : newSynonym.getName().getHybridChildRelations()){
302
            if (!rel.getHybridName().isPersited()) {
303
                nameService.save(rel.getHybridName());
304
            }
305
            if (!rel.getParentName().isPersited()) {
306
                nameService.save(rel.getParentName());
307
            }
308
        }
309
        Taxon acceptedTaxon = (Taxon)load(acceptedTaxonUuid);
310
        acceptedTaxon.addSynonym(newSynonym, newSynonym.getType());
311
        result.addUpdatedObject(acceptedTaxon);
312
        return result;
313

    
314
    }
315

    
316

    
317

    
318

    
319
    @Override
320
    @Transactional(readOnly = false)
321
    public UpdateResult changeSynonymToRelatedTaxon(UUID synonymUuid,
322
            UUID toTaxonUuid,
323
            TaxonRelationshipType taxonRelationshipType,
324
            Reference citation,
325
            String microcitation){
326

    
327
        UpdateResult result = new UpdateResult();
328
        Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
329
        Synonym synonym = (Synonym) dao.load(synonymUuid);
330
        result = changeSynonymToRelatedTaxon(synonym, toTaxon, taxonRelationshipType, citation, microcitation);
331
        Taxon relatedTaxon = (Taxon)result.getCdmEntity();
332
//        result.setCdmEntity(relatedTaxon);
333
        result.addUpdatedObject(relatedTaxon);
334
        result.addUpdatedObject(toTaxon);
335
        return result;
336
    }
337

    
338
    @Override
339
    @Transactional(readOnly = false)
340
    public UpdateResult changeSynonymToRelatedTaxon(Synonym synonym, Taxon toTaxon, TaxonRelationshipType taxonRelationshipType, Reference citation, String microcitation){
341
        // Get name from synonym
342
        if (synonym == null){
343
            return null;
344
        }
345

    
346
        UpdateResult result = new UpdateResult();
347

    
348
        TaxonName synonymName = synonym.getName();
349

    
350
      /*  // remove synonym from taxon
351
        toTaxon.removeSynonym(synonym);
352
*/
353
        // Create a taxon with synonym name
354
        Taxon fromTaxon = Taxon.NewInstance(synonymName, null);
355
        save(fromTaxon);
356
        fromTaxon.setAppendedPhrase(synonym.getAppendedPhrase());
357

    
358
        // Add taxon relation
359
        fromTaxon.addTaxonRelation(toTaxon, taxonRelationshipType, citation, microcitation);
360
        result.setCdmEntity(fromTaxon);
361
        // since we are swapping names, we have to detach the name from the synonym completely.
362
        // Otherwise the synonym will still be in the list of typified names.
363
       // synonym.getName().removeTaxonBase(synonym);
364
        result.includeResult(this.deleteSynonym(synonym, null));
365

    
366
        return result;
367
    }
368

    
369
    @Transactional(readOnly = false)
370
    @Override
371
    public void changeHomotypicalGroupOfSynonym(Synonym synonym, HomotypicalGroup newHomotypicalGroup,
372
            Taxon targetTaxon, boolean setBasionymRelationIfApplicable){
373
        // Get synonym name
374
        TaxonName synonymName = synonym.getName();
375
        HomotypicalGroup oldHomotypicalGroup = synonymName.getHomotypicalGroup();
376

    
377
        // Switch groups
378
        oldHomotypicalGroup.removeTypifiedName(synonymName, false);
379
        newHomotypicalGroup.addTypifiedName(synonymName);
380

    
381
        //remove existing basionym relationships
382
        synonymName.removeBasionyms();
383

    
384
        //add basionym relationship
385
        if (setBasionymRelationIfApplicable){
386
            Set<TaxonName> basionyms = newHomotypicalGroup.getBasionyms();
387
            for (TaxonName basionym : basionyms){
388
                synonymName.addBasionym(basionym);
389
            }
390
        }
391

    
392
        //set synonym relationship correctly
393
        Taxon acceptedTaxon = synonym.getAcceptedTaxon();
394

    
395
        boolean hasNewTargetTaxon = targetTaxon != null && !targetTaxon.equals(acceptedTaxon);
396
        if (acceptedTaxon != null){
397

    
398
            HomotypicalGroup acceptedGroup = acceptedTaxon.getHomotypicGroup();
399
            boolean isHomotypicToTaxon = acceptedGroup.equals(newHomotypicalGroup);
400
            SynonymType newRelationType = isHomotypicToTaxon? SynonymType.HOMOTYPIC_SYNONYM_OF() : SynonymType.HETEROTYPIC_SYNONYM_OF();
401
            synonym.setType(newRelationType);
402

    
403
            if (hasNewTargetTaxon){
404
                acceptedTaxon.removeSynonym(synonym, false);
405
            }
406
        }
407
        if (hasNewTargetTaxon ){
408
            @SuppressWarnings("null")
409
            HomotypicalGroup acceptedGroup = targetTaxon.getHomotypicGroup();
410
            boolean isHomotypicToTaxon = acceptedGroup.equals(newHomotypicalGroup);
411
            SynonymType relType = isHomotypicToTaxon? SynonymType.HOMOTYPIC_SYNONYM_OF() : SynonymType.HETEROTYPIC_SYNONYM_OF();
412
            targetTaxon.addSynonym(synonym, relType);
413
        }
414

    
415
    }
416

    
417
    @Override
418
    @Transactional(readOnly = false)
419
    public void updateTitleCache(Class<? extends TaxonBase> clazz, Integer stepSize, IIdentifiableEntityCacheStrategy<TaxonBase> cacheStrategy, IProgressMonitor monitor) {
420
        if (clazz == null){
421
            clazz = TaxonBase.class;
422
        }
423
        super.updateTitleCacheImpl(clazz, stepSize, cacheStrategy, monitor);
424
    }
425

    
426
    @Override
427
    @Autowired
428
    protected void setDao(ITaxonDao dao) {
429
        this.dao = dao;
430
    }
431

    
432
    @Override
433
    public Pager<TaxonBase> findTaxaByName(Class<? extends TaxonBase> clazz, String uninomial,	String infragenericEpithet, String specificEpithet,	String infraspecificEpithet, String authorship, Rank rank, Integer pageSize,Integer pageNumber) {
434
        if (clazz == null){
435
            clazz = TaxonBase.class;
436
        }
437
        Integer numberOfResults = dao.countTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, rank);
438

    
439
        List<TaxonBase> results = new ArrayList<>();
440
        if(numberOfResults > 0) { // no point checking again
441
            results = dao.findTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, authorship, rank, pageSize, pageNumber);
442
        }
443

    
444
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
445
    }
446

    
447
    @Override
448
    public List<TaxonBase> listTaxaByName(Class<? extends TaxonBase> clazz, String uninomial, String infragenericEpithet, String specificEpithet,	String infraspecificEpithet, String authorship, Rank rank, Integer pageSize,Integer pageNumber) {
449
        if (clazz == null){
450
            clazz = TaxonBase.class;
451
        }
452
        Integer numberOfResults = dao.countTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, rank);
453

    
454
        List<TaxonBase> results = new ArrayList<>();
455
        if(numberOfResults > 0) { // no point checking again
456
            results = dao.findTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, authorship, rank, pageSize, pageNumber);
457
        }
458

    
459
        return results;
460
    }
461

    
462
    @Override
463
    public List<TaxonRelationship> listToTaxonRelationships(Taxon taxon, TaxonRelationshipType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths){
464
        Integer numberOfResults = dao.countTaxonRelationships(taxon, type, TaxonRelationship.Direction.relatedTo);
465

    
466
        List<TaxonRelationship> results = new ArrayList<>();
467
        if(numberOfResults > 0) { // no point checking again
468
            results = dao.getTaxonRelationships(taxon, type, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedTo);
469
        }
470
        return results;
471
    }
472

    
473
    @Override
474
    public Pager<TaxonRelationship> pageToTaxonRelationships(Taxon taxon, TaxonRelationshipType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
475
        Integer numberOfResults = dao.countTaxonRelationships(taxon, type, TaxonRelationship.Direction.relatedTo);
476

    
477
        List<TaxonRelationship> results = new ArrayList<>();
478
        if(numberOfResults > 0) { // no point checking again
479
            results = dao.getTaxonRelationships(taxon, type, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedTo);
480
        }
481
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
482
    }
483

    
484
    @Override
485
    public List<TaxonRelationship> listFromTaxonRelationships(Taxon taxon, TaxonRelationshipType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths){
486
        Integer numberOfResults = dao.countTaxonRelationships(taxon, type, TaxonRelationship.Direction.relatedFrom);
487

    
488
        List<TaxonRelationship> results = new ArrayList<>();
489
        if(numberOfResults > 0) { // no point checking again
490
            results = dao.getTaxonRelationships(taxon, type, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedFrom);
491
        }
492
        return results;
493
    }
494

    
495
    @Override
496
    public Pager<TaxonRelationship> pageFromTaxonRelationships(Taxon taxon, TaxonRelationshipType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
497
        Integer numberOfResults = dao.countTaxonRelationships(taxon, type, TaxonRelationship.Direction.relatedFrom);
498

    
499
        List<TaxonRelationship> results = new ArrayList<>();
500
        if(numberOfResults > 0) { // no point checking again
501
            results = dao.getTaxonRelationships(taxon, type, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedFrom);
502
        }
503
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
504
    }
505

    
506
    @Override
507
    public List<TaxonRelationship> listTaxonRelationships(Set<TaxonRelationshipType> types,
508
            Integer pageSize, Integer pageStart, List<OrderHint> orderHints, List<String> propertyPaths) {
509
        Long numberOfResults = dao.countTaxonRelationships(types);
510

    
511
        List<TaxonRelationship> results = new ArrayList<>();
512
        if(numberOfResults > 0) {
513
            results = dao.getTaxonRelationships(types, pageSize, pageStart, orderHints, propertyPaths);
514
        }
515
        return results;
516
    }
517

    
518
    @Override
519
    public Taxon findAcceptedTaxonFor(UUID synonymUuid, UUID classificationUuid, List<String> propertyPaths){
520

    
521
        Taxon result = null;
522
        Long count = 0l;
523

    
524
        Synonym synonym = null;
525

    
526
        try {
527
            synonym = (Synonym) dao.load(synonymUuid);
528
        } catch (ClassCastException e){
529
            throw new EntityNotFoundException("The TaxonBase entity referenced by " + synonymUuid + " is not a Synonmy");
530
        } catch (NullPointerException e){
531
            throw new EntityNotFoundException("No TaxonBase entity found for " + synonymUuid);
532
        }
533

    
534
        Classification classificationFilter = null;
535
        if(classificationUuid != null){
536
            try {
537
            classificationFilter = classificationDao.load(classificationUuid);
538
            } catch (NullPointerException e){
539
                throw new EntityNotFoundException("No Classification entity found for " + classificationUuid);
540
            }
541
            if(classificationFilter == null){
542

    
543
            }
544
        }
545

    
546
        count = dao.countAcceptedTaxonFor(synonym, classificationFilter) ;
547
        if(count > 0){
548
            result = dao.acceptedTaxonFor(synonym, classificationFilter, propertyPaths);
549
        }
550

    
551
        return result;
552
    }
553

    
554

    
555
    @Override
556
    public Set<Taxon> listRelatedTaxa(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, Integer maxDepth,
557
            Integer limit, Integer start, List<String> propertyPaths) {
558

    
559
        Set<Taxon> relatedTaxa = collectRelatedTaxa(taxon, includeRelationships, new HashSet<>(), maxDepth);
560
        relatedTaxa.remove(taxon);
561
        beanInitializer.initializeAll(relatedTaxa, propertyPaths);
562
        return relatedTaxa;
563
    }
564

    
565

    
566
    /**
567
     * recursively collect related taxa for the given <code>taxon</code> . The returned list will also include the
568
     *  <code>taxon</code> supplied as parameter.
569
     *
570
     * @param taxon
571
     * @param includeRelationships
572
     * @param taxa
573
     * @param maxDepth can be <code>null</code> for infinite depth
574
     * @return
575
     */
576
    private Set<Taxon> collectRelatedTaxa(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, Set<Taxon> taxa, Integer maxDepth) {
577

    
578
        if(taxa.isEmpty()) {
579
            taxa.add(taxon);
580
        }
581

    
582
        if(includeRelationships.isEmpty()){
583
            return taxa;
584
        }
585

    
586
        if(maxDepth != null) {
587
            maxDepth--;
588
        }
589
        if(logger.isDebugEnabled()){
590
            logger.debug("collecting related taxa for " + taxon + " with maxDepth=" + maxDepth);
591
        }
592
        List<TaxonRelationship> taxonRelationships = dao.getTaxonRelationships(taxon, null, null, null, null, null, null);
593
        for (TaxonRelationship taxRel : taxonRelationships) {
594

    
595
            // skip invalid data
596
            if (taxRel.getToTaxon() == null || taxRel.getFromTaxon() == null || taxRel.getType() == null) {
597
                continue;
598
            }
599
            // filter by includeRelationships
600
            for (TaxonRelationshipEdge relationshipEdgeFilter : includeRelationships) {
601
                if ( relationshipEdgeFilter.getTaxonRelationshipTypes().equals(taxRel.getType()) ) {
602
                    if (relationshipEdgeFilter.getDirections().contains(Direction.relatedTo) && !taxa.contains(taxRel.getToTaxon())) {
603
                        if(logger.isDebugEnabled()){
604
                            logger.debug(maxDepth + ": " + taxon.getTitleCache() + " --[" + taxRel.getType().getLabel() + "]--> " + taxRel.getToTaxon().getTitleCache());
605
                        }
606
                        taxa.add(taxRel.getToTaxon());
607
                        if(maxDepth == null || maxDepth > 0) {
608
                            taxa.addAll(collectRelatedTaxa(taxRel.getToTaxon(), includeRelationships, taxa, maxDepth));
609
                        }
610
                    }
611
                    if(relationshipEdgeFilter.getDirections().contains(Direction.relatedFrom) && !taxa.contains(taxRel.getFromTaxon())) {
612
                        taxa.add(taxRel.getFromTaxon());
613
                        if(logger.isDebugEnabled()){
614
                            logger.debug(maxDepth + ": " +taxRel.getFromTaxon().getTitleCache() + " --[" + taxRel.getType().getLabel() + "]--> " + taxon.getTitleCache() );
615
                        }
616
                        if(maxDepth == null || maxDepth > 0) {
617
                            taxa.addAll(collectRelatedTaxa(taxRel.getFromTaxon(), includeRelationships, taxa, maxDepth));
618
                        }
619
                    }
620
                }
621
            }
622
        }
623
        return taxa;
624
    }
625

    
626
    @Override
627
    public Pager<Synonym> getSynonyms(Taxon taxon,	SynonymType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
628
        Long numberOfResults = dao.countSynonyms(taxon, type);
629

    
630
        List<Synonym> results = new ArrayList<>();
631
        if(numberOfResults > 0) { // no point checking again
632
            results = dao.getSynonyms(taxon, type, pageSize, pageNumber, orderHints, propertyPaths);
633
        }
634

    
635
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
636
    }
637

    
638
    @Override
639
    public List<List<Synonym>> getSynonymsByHomotypicGroup(Taxon taxon, List<String> propertyPaths){
640
        List<List<Synonym>> result = new ArrayList<>();
641
        taxon = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
642
        HomotypicGroupTaxonComparator comparator = new HomotypicGroupTaxonComparator(taxon);
643

    
644

    
645
        //homotypic
646
        result.add(taxon.getHomotypicSynonymsByHomotypicGroup(comparator));
647

    
648
        //heterotypic
649
        List<HomotypicalGroup> homotypicalGroups = taxon.getHeterotypicSynonymyGroups();  //currently the list is sorted by the Taxon.defaultTaxonComparator
650
        for(HomotypicalGroup homotypicalGroup : homotypicalGroups){
651
            result.add(taxon.getSynonymsInGroup(homotypicalGroup, comparator));
652
        }
653

    
654
        return result;
655

    
656
    }
657

    
658
    @Override
659
    public List<Synonym> getHomotypicSynonymsByHomotypicGroup(Taxon taxon, List<String> propertyPaths){
660
        Taxon t = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
661
        HomotypicGroupTaxonComparator comparator = new HomotypicGroupTaxonComparator(taxon);
662

    
663
        return t.getHomotypicSynonymsByHomotypicGroup(comparator);
664
    }
665

    
666
    @Override
667
    public List<List<Synonym>> getHeterotypicSynonymyGroups(Taxon taxon, List<String> propertyPaths){
668
        Taxon t = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
669
        List<HomotypicalGroup> homotypicalGroups = t.getHeterotypicSynonymyGroups();
670
        List<List<Synonym>> heterotypicSynonymyGroups = new ArrayList<>(homotypicalGroups.size());
671
        for(HomotypicalGroup homotypicalGroup : homotypicalGroups){
672
            heterotypicSynonymyGroups.add(t.getSynonymsInGroup(homotypicalGroup));
673
        }
674
        return heterotypicSynonymyGroups;
675
    }
676

    
677
    @Override
678
    public List<UuidAndTitleCache<IdentifiableEntity>> findTaxaAndNamesForEditor(IFindTaxaAndNamesConfigurator configurator){
679

    
680
        List<UuidAndTitleCache<IdentifiableEntity>> results = new ArrayList<>();
681

    
682

    
683
        if (configurator.isDoSynonyms() || configurator.isDoTaxa() || configurator.isDoNamesWithoutTaxa() || configurator.isDoTaxaByCommonNames()){
684
        	results = dao.getTaxaByNameForEditor(configurator.isDoTaxa(), configurator.isDoSynonyms(), configurator.isDoNamesWithoutTaxa(), configurator.isDoMisappliedNames(), configurator.isDoTaxaByCommonNames(), configurator.getTitleSearchStringSqlized(), configurator.getClassification(), configurator.getMatchMode(), configurator.getNamedAreas(), configurator.getOrder());
685
        }
686

    
687
        return results;
688
    }
689

    
690
    @Override
691
    public Pager<IdentifiableEntity> findTaxaAndNames(IFindTaxaAndNamesConfigurator configurator) {
692

    
693
        List<IdentifiableEntity> results = new ArrayList<>();
694
        long numberOfResults = 0; // overall number of results (as opposed to number of results per page)
695
        List<TaxonBase> taxa = null;
696

    
697
        // Taxa and synonyms
698
        long numberTaxaResults = 0L;
699

    
700

    
701
        List<String> propertyPath = new ArrayList<>();
702
        if(configurator.getTaxonPropertyPath() != null){
703
            propertyPath.addAll(configurator.getTaxonPropertyPath());
704
        }
705

    
706

    
707
       if (configurator.isDoMisappliedNames() || configurator.isDoSynonyms() || configurator.isDoTaxa() || configurator.isDoTaxaByCommonNames()){
708
            if(configurator.getPageSize() != null){ // no point counting if we need all anyway
709
                numberTaxaResults =
710
                    dao.countTaxaByName(configurator.isDoTaxa(),configurator.isDoSynonyms(), configurator.isDoMisappliedNames(),
711
                        configurator.isDoTaxaByCommonNames(), configurator.isDoIncludeAuthors(), configurator.getTitleSearchStringSqlized(),
712
                        configurator.getClassification(), configurator.getMatchMode(),
713
                        configurator.getNamedAreas());
714
            }
715

    
716
            if(configurator.getPageSize() == null || numberTaxaResults > configurator.getPageSize() * configurator.getPageNumber()){ // no point checking again if less results
717
                taxa = dao.getTaxaByName(configurator.isDoTaxa(), configurator.isDoSynonyms(),
718
                    configurator.isDoMisappliedNames(), configurator.isDoTaxaByCommonNames(), configurator.isDoIncludeAuthors(),
719
                    configurator.getTitleSearchStringSqlized(), configurator.getClassification(),
720
                    configurator.getMatchMode(), configurator.getNamedAreas(), configurator.getOrder(),
721
                    configurator.getPageSize(), configurator.getPageNumber(), propertyPath);
722
            }
723
       }
724

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

    
727
        if(taxa != null){
728
            results.addAll(taxa);
729
        }
730

    
731
        numberOfResults += numberTaxaResults;
732

    
733
        // Names without taxa
734
        if (configurator.isDoNamesWithoutTaxa()) {
735
            int numberNameResults = 0;
736

    
737
            List<? extends TaxonName> names =
738
                nameDao.findByName(configurator.isDoIncludeAuthors(), configurator.getTitleSearchStringSqlized(), configurator.getMatchMode(),
739
                        configurator.getPageSize(), configurator.getPageNumber(), null, configurator.getTaxonNamePropertyPath());
740
            if (logger.isDebugEnabled()) { logger.debug(names.size() + " matching name(s) found"); }
741
            if (names.size() > 0) {
742
                for (TaxonName taxonName : names) {
743
                    if (taxonName.getTaxonBases().size() == 0) {
744
                        results.add(taxonName);
745
                        numberNameResults++;
746
                    }
747
                }
748
                if (logger.isDebugEnabled()) { logger.debug(numberNameResults + " matching name(s) without taxa found"); }
749
                numberOfResults += numberNameResults;
750
            }
751
        }
752

    
753

    
754

    
755
       return new DefaultPagerImpl<>
756
            (configurator.getPageNumber(), numberOfResults, configurator.getPageSize(), results);
757
    }
758

    
759
    public List<UuidAndTitleCache<TaxonBase>> getTaxonUuidAndTitleCache(Integer limit, String pattern){
760
        return dao.getUuidAndTitleCache(limit, pattern);
761
    }
762

    
763
    @Override
764
    public List<MediaRepresentation> getAllMedia(Taxon taxon, int size, int height, int widthOrDuration, String[] mimeTypes){
765
        List<MediaRepresentation> medRep = new ArrayList<>();
766
        taxon = (Taxon)dao.load(taxon.getUuid());
767
        Set<TaxonDescription> descriptions = taxon.getDescriptions();
768
        for (TaxonDescription taxDesc: descriptions){
769
            Set<DescriptionElementBase> elements = taxDesc.getElements();
770
            for (DescriptionElementBase descElem: elements){
771
                for(Media media : descElem.getMedia()){
772

    
773
                    //find the best matching representation
774
                    medRep.add(MediaUtils.findBestMatchingRepresentation(media, null, size, height, widthOrDuration, mimeTypes));
775

    
776
                }
777
            }
778
        }
779
        return medRep;
780
    }
781

    
782
    @Override
783
    public List<Media> listTaxonDescriptionMedia(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, boolean limitToGalleries, List<String> propertyPath){
784
        return listMedia(taxon, includeRelationships, limitToGalleries, true, false, false, propertyPath);
785
    }
786

    
787
    @Override
788
    public List<Media> listMedia(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships,
789
            Boolean limitToGalleries, Boolean includeTaxonDescriptions, Boolean includeOccurrences,
790
            Boolean includeTaxonNameDescriptions, List<String> propertyPath) {
791

    
792
    //    logger.setLevel(Level.TRACE);
793
//        Logger.getLogger("org.hibernate.SQL").setLevel(Level.TRACE);
794

    
795
        logger.trace("listMedia() - START");
796

    
797
        Set<Taxon> taxa = new HashSet<>();
798
        List<Media> taxonMedia = new ArrayList<>();
799
        List<Media> nonImageGalleryImages = new ArrayList<>();
800

    
801
        if (limitToGalleries == null) {
802
            limitToGalleries = false;
803
        }
804

    
805
        // --- resolve related taxa
806
        if (includeRelationships != null && ! includeRelationships.isEmpty()) {
807
            logger.trace("listMedia() - resolve related taxa");
808
            taxa = listRelatedTaxa(taxon, includeRelationships, null, null, null, null);
809
        }
810

    
811
        taxa.add((Taxon) dao.load(taxon.getUuid()));
812

    
813
        if(includeTaxonDescriptions != null && includeTaxonDescriptions){
814
            logger.trace("listMedia() - includeTaxonDescriptions");
815
            List<TaxonDescription> taxonDescriptions = new ArrayList<>();
816
            // --- TaxonDescriptions
817
            for (Taxon t : taxa) {
818
                taxonDescriptions.addAll(descriptionService.listTaxonDescriptions(t, null, null, null, null, propertyPath));
819
            }
820
            for (TaxonDescription taxonDescription : taxonDescriptions) {
821
                if (!limitToGalleries || taxonDescription.isImageGallery()) {
822
                    for (DescriptionElementBase element : taxonDescription.getElements()) {
823
                        for (Media media : element.getMedia()) {
824
                            if(taxonDescription.isImageGallery()){
825
                                taxonMedia.add(media);
826
                            }
827
                            else{
828
                                nonImageGalleryImages.add(media);
829
                            }
830
                        }
831
                    }
832
                }
833
            }
834
            //put images from image gallery first (#3242)
835
            taxonMedia.addAll(nonImageGalleryImages);
836
        }
837

    
838

    
839
        if(includeOccurrences != null && includeOccurrences) {
840
            logger.trace("listMedia() - includeOccurrences");
841
            Set<SpecimenOrObservationBase> specimensOrObservations = new HashSet<>();
842
            // --- Specimens
843
            for (Taxon t : taxa) {
844
                specimensOrObservations.addAll(occurrenceDao.listByAssociatedTaxon(null, t, null, null, null, null));
845
            }
846
            for (SpecimenOrObservationBase occurrence : specimensOrObservations) {
847

    
848
//            	direct media removed from specimen #3597
849
//              taxonMedia.addAll(occurrence.getMedia());
850

    
851
                // SpecimenDescriptions
852
                Set<SpecimenDescription> specimenDescriptions = occurrence.getSpecimenDescriptions();
853
                for (DescriptionBase specimenDescription : specimenDescriptions) {
854
                    if (!limitToGalleries || specimenDescription.isImageGallery()) {
855
                        Set<DescriptionElementBase> elements = specimenDescription.getElements();
856
                        for (DescriptionElementBase element : elements) {
857
                            for (Media media : element.getMedia()) {
858
                                taxonMedia.add(media);
859
                            }
860
                        }
861
                    }
862
                }
863

    
864
                if (occurrence.isInstanceOf(DerivedUnit.class)) {
865
                    DerivedUnit derivedUnit = CdmBase.deproxy(occurrence, DerivedUnit.class);
866
                    // Collection
867
                    //TODO why may collections have media attached? #
868
                    if (derivedUnit.getCollection() != null){
869
                        taxonMedia.addAll(derivedUnit.getCollection().getMedia());
870
                    }
871
                }
872
                //media in hierarchy
873
                taxonMedia.addAll(occurrenceService.getMediainHierarchy(occurrence, null, null, propertyPath).getRecords());
874
            }
875
        }
876

    
877
        if(includeTaxonNameDescriptions != null && includeTaxonNameDescriptions) {
878
            logger.trace("listMedia() - includeTaxonNameDescriptions");
879
            // --- TaxonNameDescription
880
            Set<TaxonNameDescription> nameDescriptions = new HashSet<>();
881
            for (Taxon t : taxa) {
882
                nameDescriptions .addAll(t.getName().getDescriptions());
883
            }
884
            for(TaxonNameDescription nameDescription: nameDescriptions){
885
                if (!limitToGalleries || nameDescription.isImageGallery()) {
886
                    Set<DescriptionElementBase> elements = nameDescription.getElements();
887
                    for (DescriptionElementBase element : elements) {
888
                        for (Media media : element.getMedia()) {
889
                            taxonMedia.add(media);
890
                        }
891
                    }
892
                }
893
            }
894
        }
895

    
896

    
897
        logger.trace("listMedia() - initialize");
898
        beanInitializer.initializeAll(taxonMedia, propertyPath);
899

    
900
        logger.trace("listMedia() - END");
901

    
902
        return taxonMedia;
903
    }
904

    
905
    @Override
906
    public List<TaxonBase> findTaxaByID(Set<Integer> listOfIDs) {
907
        return this.dao.loadList(listOfIDs, null);
908
    }
909

    
910
    @Override
911
    public TaxonBase findTaxonByUuid(UUID uuid, List<String> propertyPaths){
912
        return this.dao.findByUuid(uuid, null ,propertyPaths);
913
    }
914

    
915
    @Override
916
    public int countSynonyms(boolean onlyAttachedToTaxon){
917
        return this.dao.countSynonyms(onlyAttachedToTaxon);
918
    }
919

    
920
    @Override
921
    public List<TaxonName> findIdenticalTaxonNames(List<String> propertyPath) {
922
        return this.dao.findIdenticalTaxonNames(propertyPath);
923
    }
924

    
925
    @Override
926
    @Transactional(readOnly=false)
927
    public DeleteResult deleteTaxon(UUID taxonUUID, TaxonDeletionConfigurator config, UUID classificationUuid)  {
928

    
929
    	if (config == null){
930
            config = new TaxonDeletionConfigurator();
931
        }
932
    	Taxon taxon = (Taxon)dao.load(taxonUUID);
933
    	DeleteResult result = new DeleteResult();
934
    	if (taxon == null){
935
    	    result.setAbort();
936
    	    result.addException(new Exception ("The taxon was already deleted."));
937
    	    return result;
938
    	}
939
    	taxon = HibernateProxyHelper.deproxy(taxon);
940
    	Classification classification = HibernateProxyHelper.deproxy(classificationDao.load(classificationUuid), Classification.class);
941
        result = isDeletable(taxonUUID, config);
942

    
943
        if (result.isOk()){
944
            // --- DeleteSynonymRelations
945
            if (config.isDeleteSynonymRelations()){
946
                boolean removeSynonymNameFromHomotypicalGroup = false;
947
                // use tmp Set to avoid concurrent modification
948
                Set<Synonym> synsToDelete = new HashSet<>();
949
                synsToDelete.addAll(taxon.getSynonyms());
950
                for (Synonym synonym : synsToDelete){
951
                    taxon.removeSynonym(synonym, removeSynonymNameFromHomotypicalGroup);
952

    
953
                    // --- DeleteSynonymsIfPossible
954
                    if (config.isDeleteSynonymsIfPossible()){
955
                        //TODO which value
956
                        boolean newHomotypicGroupIfNeeded = true;
957
                        SynonymDeletionConfigurator synConfig = new SynonymDeletionConfigurator();
958
                        result.includeResult(deleteSynonym(synonym, synConfig));
959
                    }
960
                }
961
            }
962

    
963
            // --- DeleteTaxonRelationships
964
            if (! config.isDeleteTaxonRelationships()){
965
                if (taxon.getTaxonRelations().size() > 0){
966
                    result.setAbort();
967
                    result.addException(new Exception("Taxon can't be deleted as it is related to another taxon. " +
968
                            "Remove taxon from all relations to other taxa prior to deletion."));
969

    
970
                }
971
            } else{
972
                TaxonDeletionConfigurator configRelTaxon = new TaxonDeletionConfigurator();
973
                configRelTaxon.setDeleteTaxonNodes(false);
974
                configRelTaxon.setDeleteConceptRelationships(true);
975

    
976
                for (TaxonRelationship taxRel: taxon.getTaxonRelations()){
977
                    if (config.isDeleteMisappliedNamesAndInvalidDesignations()
978
                            && taxRel.getType().isMisappliedNameOrInvalidDesignation()
979
                            && taxon.equals(taxRel.getToTaxon())){
980
                        this.deleteTaxon(taxRel.getFromTaxon().getUuid(), config, classificationUuid);
981
                    } else if (config.isDeleteConceptRelationships() && taxRel.getType().isConceptRelationship()){
982

    
983
                        if (taxon.equals(taxRel.getToTaxon()) && isDeletable(taxRel.getFromTaxon().getUuid(), configRelTaxon).isOk()){
984
                            this.deleteTaxon(taxRel.getFromTaxon().getUuid(), configRelTaxon, classificationUuid);
985
                        }else if (isDeletable(taxRel.getToTaxon().getUuid(), configRelTaxon).isOk()){
986
                            this.deleteTaxon(taxRel.getToTaxon().getUuid(), configRelTaxon, classificationUuid);
987
                        }
988
                    }
989
                    taxon.removeTaxonRelation(taxRel);
990

    
991
                }
992
            }
993

    
994
            //    	TaxonDescription
995
            if (config.isDeleteDescriptions()){
996
                Set<TaxonDescription> descriptions = taxon.getDescriptions();
997
                List<TaxonDescription> removeDescriptions = new ArrayList<>();
998
                for (TaxonDescription desc: descriptions){
999
                    //TODO use description delete configurator ?
1000
                    //FIXME check if description is ALWAYS deletable
1001
                    if (desc.getDescribedSpecimenOrObservation() != null){
1002
                        result.setAbort();
1003
                        result.addException(new Exception("Taxon can't be deleted as it is used in a TaxonDescription" +
1004
                                " which also describes specimens or observations"));
1005
                        break;
1006
                    }
1007
                    removeDescriptions.add(desc);
1008

    
1009

    
1010
                }
1011
                if (result.isOk()){
1012
                    for (TaxonDescription desc: removeDescriptions){
1013
                        taxon.removeDescription(desc);
1014
                        descriptionService.delete(desc);
1015
                    }
1016
                } else {
1017
                    return result;
1018
                }
1019
            }
1020

    
1021

    
1022
         if (! config.isDeleteTaxonNodes() || (!config.isDeleteInAllClassifications() && classification == null && taxon.getTaxonNodes().size() > 1)){
1023
             result.addException(new Exception( "Taxon can't be deleted as it is used in more than one classification."));
1024
         }else{
1025
             if (taxon.getTaxonNodes().size() != 0){
1026
                Set<TaxonNode> nodes = taxon.getTaxonNodes();
1027
                Iterator<TaxonNode> iterator = nodes.iterator();
1028
                TaxonNode node = null;
1029
                boolean deleteChildren;
1030
                if (config.getTaxonNodeConfig().getChildHandling().equals(ChildHandling.DELETE)){
1031
                    deleteChildren = true;
1032
                }else {
1033
                    deleteChildren = false;
1034
                }
1035
                boolean success = true;
1036
                if (!config.isDeleteInAllClassifications() && !(classification == null)){
1037
                    while (iterator.hasNext()){
1038
                        node = iterator.next();
1039
                        if (node.getClassification().equals(classification)){
1040
                            break;
1041
                        }
1042
                        node = null;
1043
                    }
1044
                    if (node != null){
1045
                        HibernateProxyHelper.deproxy(node, TaxonNode.class);
1046
                        success =taxon.removeTaxonNode(node, deleteChildren);
1047
                        nodeService.delete(node);
1048
                        result.addDeletedObject(node);
1049
                    } else {
1050
                    	result.setError();
1051
                    	result.addException(new Exception("The taxon can not be deleted because it is not used in defined classification."));
1052
                    }
1053
                } else if (config.isDeleteInAllClassifications()){
1054
                    List<TaxonNode> nodesList = new ArrayList<>();
1055
                    nodesList.addAll(taxon.getTaxonNodes());
1056
                    for (ITaxonTreeNode treeNode: nodesList){
1057
                        TaxonNode taxonNode = (TaxonNode) treeNode;
1058
                        if(!deleteChildren){
1059
                            Object[] childNodes = taxonNode.getChildNodes().toArray();
1060
                            for (Object childNode: childNodes){
1061
                                TaxonNode childNodeCast = (TaxonNode) childNode;
1062
                                taxonNode.getParent().addChildNode(childNodeCast, childNodeCast.getReference(), childNodeCast.getMicroReference());
1063
                            }
1064
                        }
1065
                    }
1066
                    config.getTaxonNodeConfig().setDeleteElement(false);
1067
                    DeleteResult resultNodes = nodeService.deleteTaxonNodes(nodesList, config);
1068
                    if (!resultNodes.isOk()){
1069
                    	result.addExceptions(resultNodes.getExceptions());
1070
                    	result.setStatus(resultNodes.getStatus());
1071
                    } else {
1072
                        result.addUpdatedObjects(resultNodes.getUpdatedObjects());
1073
                    }
1074
                }
1075
                if (!success){
1076
                    result.setError();
1077
                    result.addException(new Exception("The taxon can not be deleted because the taxon node can not be removed."));
1078
                }
1079
            }
1080
         }
1081
         TaxonName name = taxon.getName();
1082
         taxon.setName(null);
1083
         this.saveOrUpdate(taxon);
1084

    
1085
         if ((taxon.getTaxonNodes() == null || taxon.getTaxonNodes().size()== 0)  && result.isOk()){
1086
             try{
1087
                 UUID uuid = dao.delete(taxon);
1088
                 result.addDeletedObject(taxon);
1089
             }catch(Exception e){
1090
                 result.addException(e);
1091
                 result.setError();
1092
             }
1093
         } else {
1094
             result.setError();
1095
             result.addException(new Exception("The Taxon can't be deleted because it is used in a classification."));
1096

    
1097
         }
1098
            //TaxonName
1099
        if (config.isDeleteNameIfPossible() && result.isOk()){
1100
            DeleteResult nameResult = new DeleteResult();
1101
            //remove name if possible (and required)
1102
            if (name != null ){
1103
                nameResult = nameService.delete(name.getUuid(), config.getNameDeletionConfig());
1104
            }
1105
            if (nameResult.isError() || nameResult.isAbort()){
1106
                result.addRelatedObject(name);
1107
                result.addExceptions(nameResult.getExceptions());
1108
            }else{
1109
                result.includeResult(nameResult);
1110
            }
1111

    
1112

    
1113
       }
1114
       }
1115

    
1116
        return result;
1117

    
1118
    }
1119

    
1120
    private String checkForReferences(Taxon taxon){
1121
        Set<CdmBase> referencingObjects = genericDao.getReferencingObjects(taxon);
1122
        for (CdmBase referencingObject : referencingObjects){
1123
            //IIdentificationKeys (Media, Polytomous, MultiAccess)
1124
            if (HibernateProxyHelper.isInstanceOf(referencingObject, IIdentificationKey.class)){
1125
                String message = "Taxon" + taxon.getTitleCache() + "can't be deleted as it is used in an identification key. Remove from identification key prior to deleting this name";
1126

    
1127
                return message;
1128
            }
1129

    
1130

    
1131
           /* //PolytomousKeyNode
1132
            if (referencingObject.isInstanceOf(PolytomousKeyNode.class)){
1133
                String message = "Taxon" + taxon.getTitleCache() + " can't be deleted as it is used in polytomous key node";
1134
                return message;
1135
            }*/
1136

    
1137
            //TaxonInteraction
1138
            if (referencingObject.isInstanceOf(TaxonInteraction.class)){
1139
                String message = "Taxon can't be deleted as it is used in taxonInteraction#taxon2";
1140
                return message;
1141
            }
1142

    
1143
          //TaxonInteraction
1144
            if (referencingObject.isInstanceOf(DeterminationEvent.class)){
1145
                String message = "Taxon can't be deleted as it is used in a determination event";
1146
                return message;
1147
            }
1148

    
1149
        }
1150

    
1151
        referencingObjects = null;
1152
        return null;
1153
    }
1154

    
1155
    private boolean checkForPolytomousKeys(Taxon taxon){
1156
        boolean result = false;
1157
        List<CdmBase> list = genericDao.getCdmBasesByFieldAndClass(PolytomousKeyNode.class, "taxon", taxon, null);
1158
        if (!list.isEmpty()) {
1159
            result = true;
1160
        }
1161
        return result;
1162
    }
1163

    
1164
    @Override
1165
    @Transactional(readOnly = false)
1166
    public DeleteResult delete(UUID synUUID){
1167
    	DeleteResult result = new DeleteResult();
1168
    	Synonym syn = (Synonym)dao.load(synUUID);
1169

    
1170
        return this.deleteSynonym(syn, null);
1171
    }
1172

    
1173
    @Override
1174
    @Transactional(readOnly = false)
1175
    public DeleteResult deleteSynonym(UUID synonymUuid, SynonymDeletionConfigurator config) {
1176
        return deleteSynonym((Synonym)dao.load(synonymUuid), config);
1177

    
1178
    }
1179

    
1180

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

    
1191
        if (config == null){
1192
            config = new SynonymDeletionConfigurator();
1193
        }
1194

    
1195
        result = isDeletable(synonym.getUuid(), config);
1196

    
1197

    
1198
        if (result.isOk()){
1199

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

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

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

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

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

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

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

    
1233
        }
1234
        return result;
1235
    }
1236

    
1237
    @Override
1238
    public List<TaxonName> findIdenticalTaxonNameIds(List<String> propertyPath) {
1239

    
1240
        return this.dao.findIdenticalNamesNew(propertyPath);
1241
    }
1242
//
1243
//    @Override
1244
//    public String getPhylumName(TaxonName name){
1245
//        return this.dao.getPhylumName(name);
1246
//    }
1247

    
1248
    @Override
1249
    public Taxon findBestMatchingTaxon(String taxonName) {
1250
        MatchingTaxonConfigurator config = MatchingTaxonConfigurator.NewInstance();
1251
        config.setTaxonNameTitle(taxonName);
1252
        return findBestMatchingTaxon(config);
1253
    }
1254

    
1255
    @Override
1256
    public Taxon findBestMatchingTaxon(MatchingTaxonConfigurator config) {
1257

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

    
1278
                    boolean newCandidateInClassification = isInClassification(newCanditate, config);
1279
                    if (! newCandidateInClassification && config.isOnlyMatchingClassificationUuid()){
1280
                        continue;
1281
                    }else if (newCandidateInClassification && ! bestCandidateIsInClassification){
1282
                        bestCandidate = newCanditate;
1283
                        countEqualCandidates = 1;
1284
                        bestCandidateIsInClassification = true;
1285
                        continue;
1286
                    }
1287
                    if (bestCandidate == null){
1288
                        bestCandidate = newCanditate;
1289
                        countEqualCandidates = 1;
1290
                        continue;
1291
                    }
1292

    
1293
                }else{  //not Taxon.class
1294
                    continue;
1295
                }
1296
                countEqualCandidates++;
1297

    
1298
            }
1299
            if (bestCandidate != null){
1300
                if(countEqualCandidates > 1){
1301
                    logger.info(countEqualCandidates + " equally matching TaxonBases found, using first accepted Taxon: " + bestCandidate.getTitleCache());
1302
                    return bestCandidate;
1303
                } else {
1304
                    logger.info("using accepted Taxon: " + bestCandidate.getTitleCache());
1305
                    return bestCandidate;
1306
                }
1307
            }
1308

    
1309

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

    
1326
        } catch (Exception e){
1327
            logger.error(e);
1328
            e.printStackTrace();
1329
        }
1330

    
1331
        return bestCandidate;
1332
    }
1333

    
1334
    private boolean isInClassification(Taxon taxon, MatchingTaxonConfigurator config) {
1335
        UUID configClassificationUuid = config.getClassificationUuid();
1336
        if (configClassificationUuid == null){
1337
            return false;
1338
        }
1339
        for (TaxonNode node : taxon.getTaxonNodes()){
1340
            UUID classUuid = node.getClassification().getUuid();
1341
            if (configClassificationUuid.equals(classUuid)){
1342
                return true;
1343
            }
1344
        }
1345
        return false;
1346
    }
1347

    
1348
    private boolean isMatchesSecUuid(Taxon taxon, MatchingTaxonConfigurator config) {
1349
        UUID configSecUuid = config.getSecUuid();
1350
        if (configSecUuid == null){
1351
            return false;
1352
        }
1353
        UUID taxonSecUuid = (taxon.getSec() == null)? null : taxon.getSec().getUuid();
1354
        return configSecUuid.equals(taxonSecUuid);
1355
    }
1356

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

    
1373
    @Override
1374
    @Transactional(readOnly = false)
1375
    public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym,
1376
            Taxon newTaxon,
1377
            boolean moveHomotypicGroup,
1378
            SynonymType newSynonymType) throws HomotypicalGroupChangeException {
1379
        return moveSynonymToAnotherTaxon(oldSynonym, newTaxon, moveHomotypicGroup,
1380
                newSynonymType,
1381
                oldSynonym.getSec(),
1382
                oldSynonym.getSecMicroReference(),
1383
                true);
1384
    }
1385

    
1386
    @Override
1387
    @Transactional(readOnly = false)
1388
    public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym,
1389
            Taxon newTaxon,
1390
            boolean moveHomotypicGroup,
1391
            SynonymType newSynonymType,
1392
            Reference newSecundum,
1393
            String newSecundumDetail,
1394
            boolean keepSecundumIfUndefined) throws HomotypicalGroupChangeException {
1395

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

    
1407
        HomotypicalGroup homotypicGroup = synonymName.getHomotypicalGroup();
1408
        int hgSize = homotypicGroup.getTypifiedNames().size();
1409
        boolean isSingleInGroup = !(hgSize > 1);
1410

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

    
1429
        UpdateResult result = new UpdateResult();
1430
        //move all synonyms to new taxon
1431
        List<Synonym> homotypicSynonyms = oldTaxon.getSynonymsInGroup(homotypicGroup);
1432
        for (Synonym synRelation: homotypicSynonyms){
1433

    
1434
            newTaxon = HibernateProxyHelper.deproxy(newTaxon, Taxon.class);
1435
            oldTaxon = HibernateProxyHelper.deproxy(oldTaxon, Taxon.class);
1436
            newTaxon.addSynonym(synRelation, newSynonymType);
1437
            oldTaxon.removeSynonym(synRelation, false);
1438
            if (newSecundum != null || !keepSecundumIfUndefined){
1439
                synRelation.setSec(newSecundum);
1440
            }
1441
            if (newSecundumDetail != null || !keepSecundumIfUndefined){
1442
                synRelation.setSecMicroReference(newSecundumDetail);
1443
            }
1444

    
1445
            //set result  //why is this needed? Seems wrong to me (AM 10.10.2016)
1446
            if (!synRelation.equals(oldSynonym)){
1447
                result.setError();
1448
            }
1449
        }
1450

    
1451
        result.addUpdatedObject(oldTaxon);
1452
        result.addUpdatedObject(newTaxon);
1453
        saveOrUpdate(oldTaxon);
1454
        saveOrUpdate(newTaxon);
1455

    
1456
        return result;
1457
    }
1458

    
1459
    @Override
1460
    public <T extends TaxonBase> List<UuidAndTitleCache<T>> getUuidAndTitleCache(Class<T> clazz, Integer limit, String pattern) {
1461
        return dao.getUuidAndTitleCache(clazz, limit, pattern);
1462
    }
1463

    
1464
    @Override
1465
    public Pager<SearchResult<TaxonBase>> findByFullText(
1466
            Class<? extends TaxonBase> clazz, String queryString,
1467
            Classification classification, List<Language> languages,
1468
            boolean highlightFragments, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
1469

    
1470

    
1471
        LuceneSearch luceneSearch = prepareFindByFullTextSearch(clazz, queryString, classification, languages, highlightFragments, null);
1472

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

    
1483
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1484
        idFieldMap.put(CdmBaseType.TAXON, "id");
1485

    
1486
        // ---  initialize taxa, thighlight matches ....
1487
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1488
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1489
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1490

    
1491
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
1492
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
1493
    }
1494

    
1495
    @Override
1496
    public Pager<SearchResult<TaxonBase>> findByDistribution(List<NamedArea> areaFilter, List<PresenceAbsenceTerm> statusFilter,
1497
            Classification classification,
1498
            Integer pageSize, Integer pageNumber,
1499
            List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
1500

    
1501
        LuceneSearch luceneSearch = prepareByDistributionSearch(areaFilter, statusFilter, classification);
1502

    
1503
        // --- execute search
1504
        TopGroups<BytesRef> topDocsResultSet;
1505
        try {
1506
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
1507
        } catch (ParseException e) {
1508
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
1509
            luceneParseException.setStackTrace(e.getStackTrace());
1510
            throw luceneParseException;
1511
        }
1512

    
1513
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1514
        idFieldMap.put(CdmBaseType.TAXON, "id");
1515

    
1516
        // ---  initialize taxa, thighlight matches ....
1517
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1518
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1519
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1520

    
1521
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
1522
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
1523
    }
1524

    
1525
    /**
1526
     * @param clazz
1527
     * @param queryString
1528
     * @param classification
1529
     * @param languages
1530
     * @param highlightFragments
1531
     * @param sortFields TODO
1532
     * @param directorySelectClass
1533
     * @return
1534
     */
1535
    protected LuceneSearch prepareFindByFullTextSearch(Class<? extends CdmBase> clazz, String queryString, Classification classification, List<Language> languages,
1536
            boolean highlightFragments, SortField[] sortFields) {
1537
        Builder finalQueryBuilder = new Builder();
1538
        Builder textQueryBuilder = new Builder();
1539

    
1540
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1541
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1542

    
1543
        if(sortFields == null){
1544
            sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING,  false)};
1545
        }
1546
        luceneSearch.setSortFields(sortFields);
1547

    
1548
        // ---- search criteria
1549
        luceneSearch.setCdmTypRestriction(clazz);
1550

    
1551
        if(!queryString.isEmpty() && !queryString.equals("*") && !queryString.equals("?") ) {
1552
            textQueryBuilder.add(taxonBaseQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
1553
            textQueryBuilder.add(taxonBaseQueryFactory.newDefinedTermQuery("name.rank", queryString, languages), Occur.SHOULD);
1554
        }
1555

    
1556
        BooleanQuery textQuery = textQueryBuilder.build();
1557
        if(textQuery.clauses().size() > 0) {
1558
            finalQueryBuilder.add(textQuery, Occur.MUST);
1559
        }
1560

    
1561

    
1562
        if(classification != null){
1563
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery("taxonNodes.classification.id", classification), Occur.MUST);
1564
        }
1565
        luceneSearch.setQuery(finalQueryBuilder.build());
1566

    
1567
        if(highlightFragments){
1568
            luceneSearch.setHighlightFields(taxonBaseQueryFactory.getTextFieldNamesAsArray());
1569
        }
1570
        return luceneSearch;
1571
    }
1572

    
1573
    /**
1574
     * Uses org.apache.lucene.search.join.JoinUtil for query time joining, alternatively
1575
     * the BlockJoinQuery could be used. The latter might be more memory save but has the
1576
     * drawback of requiring to do the join an indexing time.
1577
     * see  http://dev.e-taxonomy.eu/trac/wiki/LuceneNotes#JoinsinLucene for more information on this.
1578
     *
1579
     * Joins TaxonRelationShip with Taxon depending on the direction of the given edge:
1580
     * <ul>
1581
     * <li>direct, everted: {@link Direction.relatedTo}: TaxonRelationShip.relatedTo.id --&gt; Taxon.id </li>
1582
     * <li>inverse: {@link Direction.relatedFrom}:  TaxonRelationShip.relatedFrom.id --&gt; Taxon.id </li>
1583
     * <ul>
1584
     * @param queryString
1585
     * @param classification
1586
     * @param languages
1587
     * @param highlightFragments
1588
     * @param sortFields TODO
1589
     *
1590
     * @return
1591
     * @throws IOException
1592
     */
1593
    protected LuceneSearch prepareFindByTaxonRelationFullTextSearch(TaxonRelationshipEdge edge, String queryString, Classification classification, List<Language> languages,
1594
            boolean highlightFragments, SortField[] sortFields) throws IOException {
1595

    
1596
        String fromField;
1597
        String queryTermField;
1598
        String toField = "id"; // TaxonBase.uuid
1599

    
1600
        if(edge.isBidirectional()){
1601
            throw new RuntimeException("Bidirectional joining not supported!");
1602
        }
1603
        if(edge.isEvers()){
1604
            fromField = "relatedFrom.id";
1605
            queryTermField = "relatedFrom.titleCache";
1606
        } else if(edge.isInvers()) {
1607
            fromField = "relatedTo.id";
1608
            queryTermField = "relatedTo.titleCache";
1609
        } else {
1610
            throw new RuntimeException("Invalid direction: " + edge.getDirections());
1611
        }
1612

    
1613
        Builder finalQueryBuilder = new Builder();
1614

    
1615
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1616
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1617

    
1618
        Builder joinFromQueryBuilder = new Builder();
1619
        joinFromQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(queryTermField, queryString), Occur.MUST);
1620
        joinFromQueryBuilder.add(taxonBaseQueryFactory.newEntityIdsQuery("type.id", edge.getTaxonRelationshipTypes()), Occur.MUST);
1621

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

    
1624
        if(sortFields == null){
1625
            sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING,  false)};
1626
        }
1627
        luceneSearch.setSortFields(sortFields);
1628

    
1629
        finalQueryBuilder.add(joinQuery, Occur.MUST);
1630

    
1631
        if(classification != null){
1632
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery("taxonNodes.classification.id", classification), Occur.MUST);
1633
        }
1634
        luceneSearch.setQuery(finalQueryBuilder.build());
1635

    
1636
        if(highlightFragments){
1637
            luceneSearch.setHighlightFields(taxonBaseQueryFactory.getTextFieldNamesAsArray());
1638
        }
1639
        return luceneSearch;
1640
    }
1641

    
1642
    @Override
1643
    public Pager<SearchResult<TaxonBase>> findTaxaAndNamesByFullText(
1644
            EnumSet<TaxaAndNamesSearchMode> searchModes, String queryString, Classification classification,
1645
            Set<NamedArea> namedAreas, Set<PresenceAbsenceTerm> distributionStatus, List<Language> languages,
1646
            boolean highlightFragments, Integer pageSize,
1647
            Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths)
1648
            throws IOException, LuceneParseException, LuceneMultiSearchException {
1649

    
1650
        // FIXME: allow taxonomic ordering
1651
        //  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";
1652
        // this require building a special sort column by a special classBridge
1653
        if(highlightFragments){
1654
            logger.warn("findTaxaAndNamesByFullText() : fragment highlighting is " +
1655
                    "currently not fully supported by this method and thus " +
1656
                    "may not work with common names and misapplied names.");
1657
        }
1658

    
1659
        // convert sets to lists
1660
        List<NamedArea> namedAreaList = null;
1661
        List<PresenceAbsenceTerm>distributionStatusList = null;
1662
        if(namedAreas != null){
1663
            namedAreaList = new ArrayList<>(namedAreas.size());
1664
            namedAreaList.addAll(namedAreas);
1665
        }
1666
        if(distributionStatus != null){
1667
            distributionStatusList = new ArrayList<>(distributionStatus.size());
1668
            distributionStatusList.addAll(distributionStatus);
1669
        }
1670

    
1671
        // set default if parameter is null
1672
        if(searchModes == null){
1673
            searchModes = EnumSet.of(TaxaAndNamesSearchMode.doTaxa);
1674
        }
1675

    
1676
        // set sort order and thus override any sort orders which may have been
1677
        // defined by prepare*Search methods
1678
        if(orderHints == null){
1679
            orderHints = OrderHint.NOMENCLATURAL_SORT_ORDER.asList();
1680
        }
1681
        SortField[] sortFields = new SortField[orderHints.size()];
1682
        int i = 0;
1683
        for(OrderHint oh : orderHints){
1684
            sortFields[i++] = oh.toSortField();
1685
        }
1686
//        SortField[] sortFields = new SortField[]{SortField.FIELD_SCORE, new SortField("id", SortField.STRING, false)};
1687
//        SortField[] sortFields = new SortField[]{new SortField(NomenclaturalSortOrderBrigde.NAME_SORT_FIELD_NAME, SortField.STRING, false)};
1688

    
1689

    
1690
        boolean addDistributionFilter = namedAreas != null && namedAreas.size() > 0;
1691

    
1692
        List<LuceneSearch> luceneSearches = new ArrayList<>();
1693
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1694

    
1695
        /*
1696
          ======== filtering by distribution , HOWTO ========
1697

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

    
1703

    
1704
          3. how does it work in spatial?
1705
          see
1706
           - http://www.nsshutdown.com/projects/lucene/whitepaper/locallucene_v2.html
1707
           - http://www.infoq.com/articles/LuceneSpatialSupport
1708
           - http://www.mhaller.de/archives/156-Spatial-search-with-Lucene.html
1709
          ------------------------------------------------------------------------
1710

    
1711
          filter strategies:
1712
          A) use a separate distribution filter per index sub-query/search:
1713
           - byTaxonSyonym (query TaxaonBase):
1714
               use a join area filter (Distribution -> TaxonBase)
1715
           - byCommonName (query DescriptionElementBase): use an area filter on
1716
               DescriptionElementBase !!! PROBLEM !!!
1717
               This cannot work since the distributions are different entities than the
1718
               common names and thus these are different lucene documents.
1719
           - byMisaplliedNames (join query TaxonRelationship -> TaxaonBase):
1720
               use a join area filter (Distribution -> TaxonBase)
1721

    
1722
          B) use a common distribution filter for all index sub-query/searches:
1723
           - use a common join area filter (Distribution -> TaxonBase)
1724
           - also implement the byCommonName as join query (CommonName -> TaxonBase)
1725
           PROBLEM in this case: we are losing the fragment highlighting for the
1726
           common names, since the returned documents are always TaxonBases
1727
        */
1728

    
1729
        /* The QueryFactory for creating filter queries on Distributions should
1730
         * The query factory used for the common names query cannot be reused
1731
         * for this case, since we want to only record the text fields which are
1732
         * actually used in the primary query
1733
         */
1734
        QueryFactory distributionFilterQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Distribution.class);
1735

    
1736
        Builder multiIndexByAreaFilterBuilder = new Builder();
1737

    
1738
        // search for taxa or synonyms
1739
        if(searchModes.contains(TaxaAndNamesSearchMode.doTaxa) || searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)) {
1740
            Class taxonBaseSubclass = TaxonBase.class;
1741
            if(searchModes.contains(TaxaAndNamesSearchMode.doTaxa) && !searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
1742
                taxonBaseSubclass = Taxon.class;
1743
            } else if (!searchModes.contains(TaxaAndNamesSearchMode.doTaxa) && searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)) {
1744
                taxonBaseSubclass = Synonym.class;
1745
            }
1746
            luceneSearches.add(prepareFindByFullTextSearch(taxonBaseSubclass, queryString, classification, languages, highlightFragments, sortFields));
1747
            idFieldMap.put(CdmBaseType.TAXON, "id");
1748
            /* A) does not work!!!!
1749
            if(addDistributionFilter){
1750
                // in this case we need a filter which uses a join query
1751
                // to get the TaxonBase documents for the DescriptionElementBase documents
1752
                // which are matching the areas in question
1753
                Query taxonAreaJoinQuery = createByDistributionJoinQuery(
1754
                        namedAreaList,
1755
                        distributionStatusList,
1756
                        distributionFilterQueryFactory
1757
                        );
1758
                multiIndexByAreaFilter.add(new QueryWrapperFilter(taxonAreaJoinQuery), Occur.SHOULD);
1759
            }
1760
            */
1761
            if(addDistributionFilter && searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
1762
                // add additional area filter for synonyms
1763
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1764
                String toField = "accTaxon.id"; // id in TaxonBase index (is multivalued)
1765

    
1766
                //TODO replace by createByDistributionJoinQuery
1767
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
1768
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class, fromField, true, byDistributionQuery, toField, Taxon.class, ScoreMode.None);
1769
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1770

    
1771
            }
1772
        }
1773

    
1774
        // search by CommonTaxonName
1775
        if(searchModes.contains(TaxaAndNamesSearchMode.doTaxaByCommonNames)) {
1776
            // B)
1777
            QueryFactory descriptionElementQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(DescriptionElementBase.class);
1778
            Query byCommonNameJoinQuery = descriptionElementQueryFactory.newJoinQuery(
1779
                    CommonTaxonName.class,
1780
                    "inDescription.taxon.id",
1781
                    true,
1782
                    QueryFactory.addTypeRestriction(
1783
                                createByDescriptionElementFullTextQuery(queryString, classification, null, languages, descriptionElementQueryFactory)
1784
                                , CommonTaxonName.class
1785
                                ).build(), "id", null, ScoreMode.Max);
1786
            logger.debug("byCommonNameJoinQuery: " + byCommonNameJoinQuery.toString());
1787
            LuceneSearch byCommonNameSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
1788
            byCommonNameSearch.setCdmTypRestriction(Taxon.class);
1789
            byCommonNameSearch.setQuery(byCommonNameJoinQuery);
1790
            byCommonNameSearch.setSortFields(sortFields);
1791

    
1792
            DuplicateFilter df = new DuplicateFilter("inDescription.taxon.id");
1793
            Set<String> results=new HashSet<>();
1794
//            ScoreDoc[] hits = searcher.search(tq,df, 1000).scoreDocs;
1795
//
1796
//            byCommonNameSearch.setFilter(df);
1797
            idFieldMap.put(CdmBaseType.TAXON, "id");
1798

    
1799
            luceneSearches.add(byCommonNameSearch);
1800

    
1801
            /* A) does not work!!!!
1802
            luceneSearches.add(
1803
                    prepareByDescriptionElementFullTextSearch(CommonTaxonName.class,
1804
                            queryString, classification, null, languages, highlightFragments)
1805
                        );
1806
            idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
1807
            if(addDistributionFilter){
1808
                // in this case we are able to use DescriptionElementBase documents
1809
                // which are matching the areas in question directly
1810
                BooleanQuery byDistributionQuery = createByDistributionQuery(
1811
                        namedAreaList,
1812
                        distributionStatusList,
1813
                        distributionFilterQueryFactory
1814
                        );
1815
                multiIndexByAreaFilter.add(new QueryWrapperFilter(byDistributionQuery), Occur.SHOULD);
1816
            } */
1817
        }
1818

    
1819
        // search by misapplied names
1820
        if(searchModes.contains(TaxaAndNamesSearchMode.doMisappliedNames)) {
1821
            // NOTE:
1822
            // prepareFindByTaxonRelationFullTextSearch() is making use of JoinUtil.createJoinQuery()
1823
            // which allows doing query time joins
1824
            // finds the misapplied name (Taxon B) which is an misapplication for
1825
            // a related Taxon A.
1826
            //
1827
            luceneSearches.add(prepareFindByTaxonRelationFullTextSearch(
1828
                    new TaxonRelationshipEdge(TaxonRelationshipType.allMisappliedNameTypes(), Direction.relatedTo),
1829
                    queryString, classification, languages, highlightFragments, sortFields));
1830
            idFieldMap.put(CdmBaseType.TAXON, "id");
1831

    
1832
            if(addDistributionFilter){
1833
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1834

    
1835
                /*
1836
                 * Here i was facing wired and nasty bug which took me bugging be really for hours until I found this solution.
1837
                 * Maybe this is a bug in java itself java.
1838
                 *
1839
                 * When the string toField is constructed by using the expression TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString()
1840
                 * directly:
1841
                 *
1842
                 *    String toField = "relation." + TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString() +".to.id";
1843
                 *
1844
                 * The byDistributionQuery fails, however when the uuid is first stored in another string variable the query
1845
                 * will execute as expected:
1846
                 *
1847
                 *    String misappliedNameForUuid = TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString();
1848
                 *    String toField = "relation." + misappliedNameForUuid +".to.id";
1849
                 *
1850
                 * Comparing both strings by the String.equals method returns true, so both String are identical.
1851
                 *
1852
                 * The bug occurs when running eu.etaxonomy.cdm.api.service.TaxonServiceSearchTest in eclipse and in maven and seems to to be
1853
                 * 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)
1854
                 * The bug is persistent after a reboot of the development computer.
1855
                 */
1856
//                String misappliedNameForUuid = TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString();
1857
//                String toField = "relation." + misappliedNameForUuid +".to.id";
1858
                String toField = "relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id";
1859
//                System.out.println("relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id".equals("relation." + misappliedNameForUuid +".to.id") ? " > identical" : " > different");
1860
//                System.out.println("relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id".equals("relation." + TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString() +".to.id") ? " > identical" : " > different");
1861

    
1862
                //TODO replace by createByDistributionJoinQuery
1863
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
1864
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class, fromField, true, byDistributionQuery, toField, null, ScoreMode.None);
1865

    
1866
//                debug code for bug described above
1867
                //does not compile anymore since changing from lucene 3.6.2 to lucene 4.10+
1868
//                DocIdSet filterMatchSet = filter.getDocIdSet(luceneIndexToolProvider.getIndexReaderFor(Taxon.class));
1869
//                System.err.println(DocIdBitSetPrinter.docsAsString(filterMatchSet, 100));
1870

    
1871
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1872
            }
1873
        }
1874

    
1875
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider,
1876
                luceneSearches.toArray(new LuceneSearch[luceneSearches.size()]));
1877

    
1878

    
1879
        if(addDistributionFilter){
1880

    
1881
            // B)
1882
            // in this case we need a filter which uses a join query
1883
            // to get the TaxonBase documents for the DescriptionElementBase documents
1884
            // which are matching the areas in question
1885
            //
1886
            // for toTaxa, doByCommonName
1887
            Query taxonAreaJoinQuery = createByDistributionJoinQuery(
1888
                    namedAreaList,
1889
                    distributionStatusList,
1890
                    distributionFilterQueryFactory,
1891
                    Taxon.class, true
1892
                    );
1893
            multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1894
        }
1895

    
1896
        if (addDistributionFilter){
1897
            multiSearch.setFilter(multiIndexByAreaFilterBuilder.build());
1898
        }
1899

    
1900

    
1901
        // --- execute search
1902
        TopGroups<BytesRef> topDocsResultSet;
1903
        try {
1904
            topDocsResultSet = multiSearch.executeSearch(pageSize, pageNumber);
1905
        } catch (ParseException e) {
1906
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
1907
            luceneParseException.setStackTrace(e.getStackTrace());
1908
            throw luceneParseException;
1909
        }
1910

    
1911
        // --- initialize taxa, highlight matches ....
1912
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
1913

    
1914

    
1915
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1916
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1917

    
1918
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
1919
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
1920
    }
1921

    
1922
    /**
1923
     * @param namedAreaList at least one area must be in the list
1924
     * @param distributionStatusList optional
1925
     * @param toType toType
1926
     *      Optional parameter. Only used for debugging to print the toType documents
1927
     * @param asFilter TODO
1928
     * @return
1929
     * @throws IOException
1930
     */
1931
    protected Query createByDistributionJoinQuery(
1932
            List<NamedArea> namedAreaList,
1933
            List<PresenceAbsenceTerm> distributionStatusList,
1934
            QueryFactory queryFactory, Class<? extends CdmBase> toType, boolean asFilter
1935
            ) throws IOException {
1936

    
1937
        String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1938
        String toField = "id"; // id in toType usually this is the TaxonBase index
1939

    
1940
        BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, queryFactory);
1941

    
1942
        ScoreMode scoreMode = asFilter ?  ScoreMode.None : ScoreMode.Max;
1943

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

    
1946
        return taxonAreaJoinQuery;
1947
    }
1948

    
1949
    /**
1950
     * @param namedAreaList
1951
     * @param distributionStatusList
1952
     * @param queryFactory
1953
     * @return
1954
     */
1955
    private BooleanQuery createByDistributionQuery(List<NamedArea> namedAreaList,
1956
            List<PresenceAbsenceTerm> distributionStatusList, QueryFactory queryFactory) {
1957
        Builder areaQueryBuilder = new Builder();
1958
        // area field from Distribution
1959
        areaQueryBuilder.add(queryFactory.newEntityIdsQuery("area.id", namedAreaList), Occur.MUST);
1960

    
1961
        // status field from Distribution
1962
        if(distributionStatusList != null && distributionStatusList.size() > 0){
1963
            areaQueryBuilder.add(queryFactory.newEntityIdsQuery("status.id", distributionStatusList), Occur.MUST);
1964
        }
1965

    
1966
        BooleanQuery areaQuery = areaQueryBuilder.build();
1967
        logger.debug("createByDistributionQuery() query: " + areaQuery.toString());
1968
        return areaQuery;
1969
    }
1970

    
1971
    /**
1972
     * This method has been primarily created for testing the area join query but might
1973
     * also be useful in other situations
1974
     *
1975
     * @param namedAreaList
1976
     * @param distributionStatusList
1977
     * @param classification
1978
     * @param highlightFragments
1979
     * @return
1980
     * @throws IOException
1981
     */
1982
    protected LuceneSearch prepareByDistributionSearch(
1983
            List<NamedArea> namedAreaList, List<PresenceAbsenceTerm> distributionStatusList,
1984
            Classification classification) throws IOException {
1985

    
1986
        Builder finalQueryBuilder = new Builder();
1987

    
1988
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
1989

    
1990
        // FIXME is this query factory using the wrong type?
1991
        QueryFactory taxonQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Taxon.class);
1992

    
1993
        SortField[] sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING, false)};
1994
        luceneSearch.setSortFields(sortFields);
1995

    
1996

    
1997
        Query byAreaQuery = createByDistributionJoinQuery(namedAreaList, distributionStatusList, taxonQueryFactory, null, false);
1998

    
1999
        finalQueryBuilder.add(byAreaQuery, Occur.MUST);
2000

    
2001
        if(classification != null){
2002
            finalQueryBuilder.add(taxonQueryFactory.newEntityIdQuery("taxonNodes.classification.id", classification), Occur.MUST);
2003
        }
2004
        BooleanQuery finalQuery = finalQueryBuilder.build();
2005
        logger.info("prepareByAreaSearch() query: " + finalQuery.toString());
2006
        luceneSearch.setQuery(finalQuery);
2007

    
2008
        return luceneSearch;
2009
    }
2010

    
2011
    @Override
2012
    public Pager<SearchResult<TaxonBase>> findByDescriptionElementFullText(
2013
            Class<? extends DescriptionElementBase> clazz, String queryString,
2014
            Classification classification, List<Feature> features, List<Language> languages,
2015
            boolean highlightFragments, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
2016

    
2017

    
2018
        LuceneSearch luceneSearch = prepareByDescriptionElementFullTextSearch(clazz, queryString, classification, features, languages, highlightFragments);
2019

    
2020
        // --- execute search
2021
        TopGroups<BytesRef> topDocsResultSet;
2022
        try {
2023
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
2024
        } catch (ParseException e) {
2025
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2026
            luceneParseException.setStackTrace(e.getStackTrace());
2027
            throw luceneParseException;
2028
        }
2029

    
2030
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
2031
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2032

    
2033
        // --- initialize taxa, highlight matches ....
2034
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
2035
        @SuppressWarnings("rawtypes")
2036
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2037
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2038

    
2039
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
2040
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
2041

    
2042
    }
2043

    
2044

    
2045
    @Override
2046
    public Pager<SearchResult<TaxonBase>> findByEverythingFullText(String queryString,
2047
            Classification classification, List<Language> languages, boolean highlightFragments,
2048
            Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException, LuceneMultiSearchException {
2049

    
2050
        LuceneSearch luceneSearchByDescriptionElement = prepareByDescriptionElementFullTextSearch(null, queryString, classification, null, languages, highlightFragments);
2051
        LuceneSearch luceneSearchByTaxonBase = prepareFindByFullTextSearch(null, queryString, classification, languages, highlightFragments, null);
2052

    
2053
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider, luceneSearchByDescriptionElement, luceneSearchByTaxonBase);
2054

    
2055
        // --- execute search
2056
        TopGroups<BytesRef> topDocsResultSet;
2057
        try {
2058
            topDocsResultSet = multiSearch.executeSearch(pageSize, pageNumber);
2059
        } catch (ParseException e) {
2060
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2061
            luceneParseException.setStackTrace(e.getStackTrace());
2062
            throw luceneParseException;
2063
        }
2064

    
2065
        // --- initialize taxa, highlight matches ....
2066
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
2067

    
2068
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
2069
        idFieldMap.put(CdmBaseType.TAXON, "id");
2070
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2071

    
2072
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2073
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2074

    
2075
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
2076
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
2077

    
2078
    }
2079

    
2080

    
2081
    /**
2082
     * @param clazz
2083
     * @param queryString
2084
     * @param classification
2085
     * @param features
2086
     * @param languages
2087
     * @param highlightFragments
2088
     * @param directorySelectClass
2089
     * @return
2090
     */
2091
    protected LuceneSearch prepareByDescriptionElementFullTextSearch(Class<? extends CdmBase> clazz,
2092
            String queryString, Classification classification, List<Feature> features,
2093
            List<Language> languages, boolean highlightFragments) {
2094

    
2095
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, DescriptionElementBase.class);
2096
        QueryFactory descriptionElementQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(DescriptionElementBase.class);
2097

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

    
2100
        BooleanQuery finalQuery = createByDescriptionElementFullTextQuery(queryString, classification, features,
2101
                languages, descriptionElementQueryFactory);
2102

    
2103
        luceneSearch.setSortFields(sortFields);
2104
        luceneSearch.setCdmTypRestriction(clazz);
2105
        luceneSearch.setQuery(finalQuery);
2106
        if(highlightFragments){
2107
            luceneSearch.setHighlightFields(descriptionElementQueryFactory.getTextFieldNamesAsArray());
2108
        }
2109

    
2110
        return luceneSearch;
2111
    }
2112

    
2113
    /**
2114
     * @param queryString
2115
     * @param classification
2116
     * @param features
2117
     * @param languages
2118
     * @param descriptionElementQueryFactory
2119
     * @return
2120
     */
2121
    private BooleanQuery createByDescriptionElementFullTextQuery(String queryString, Classification classification,
2122
            List<Feature> features, List<Language> languages, QueryFactory descriptionElementQueryFactory) {
2123
        Builder finalQueryBuilder = new Builder();
2124
        Builder textQueryBuilder = new Builder();
2125
        textQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
2126

    
2127
        // common name
2128
        Builder nameQueryBuilder = new Builder();
2129
        if(languages == null || languages.size() == 0){
2130
            nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2131
        } else {
2132
            Builder languageSubQueryBuilder = new Builder();
2133
            for(Language lang : languages){
2134
                languageSubQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("language.uuid",  lang.getUuid().toString(), false), Occur.SHOULD);
2135
            }
2136
            nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2137
            nameQueryBuilder.add(languageSubQueryBuilder.build(), Occur.MUST);
2138
        }
2139
        textQueryBuilder.add(nameQueryBuilder.build(), Occur.SHOULD);
2140

    
2141

    
2142
        // text field from TextData
2143
        textQueryBuilder.add(descriptionElementQueryFactory.newMultilanguageTextQuery("text", queryString, languages), Occur.SHOULD);
2144

    
2145
        // --- TermBase fields - by representation ----
2146
        // state field from CategoricalData
2147
        textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.state", queryString, languages), Occur.SHOULD);
2148

    
2149
        // state field from CategoricalData
2150
        textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.modifyingText", queryString, languages), Occur.SHOULD);
2151

    
2152
        // area field from Distribution
2153
        textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("area", queryString, languages), Occur.SHOULD);
2154

    
2155
        // status field from Distribution
2156
        textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("status", queryString, languages), Occur.SHOULD);
2157

    
2158
        finalQueryBuilder.add(textQueryBuilder.build(), Occur.MUST);
2159
        // --- classification ----
2160

    
2161
        if(classification != null){
2162
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityIdQuery("inDescription.taxon.taxonNodes.classification.id", classification), Occur.MUST);
2163
        }
2164

    
2165
        // --- IdentifieableEntity fields - by uuid
2166
        if(features != null && features.size() > 0 ){
2167
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityUuidsQuery("feature.uuid", features), Occur.MUST);
2168
        }
2169

    
2170
        // the description must be associated with a taxon
2171
        finalQueryBuilder.add(descriptionElementQueryFactory.newIsNotNullQuery("inDescription.taxon.id"), Occur.MUST);
2172

    
2173
        BooleanQuery finalQuery = finalQueryBuilder.build();
2174
        logger.info("prepareByDescriptionElementFullTextSearch() query: " + finalQuery.toString());
2175
        return finalQuery;
2176
    }
2177

    
2178
    /**
2179
     * DefinedTerm representations and MultilanguageString maps are stored in the Lucene index by the {@link DefinedTermBaseClassBridge}
2180
     * and {@link MultilanguageTextFieldBridge } in a consistent way. One field per language and also in one additional field for all languages.
2181
     * This method is a convenient means to retrieve a Lucene query string for such the fields.
2182
     *
2183
     * @param name name of the term field as in the Lucene index. Must be field created by {@link DefinedTermBaseClassBridge}
2184
     * or {@link MultilanguageTextFieldBridge }
2185
     * @param languages the languages to search for exclusively. Can be <code>null</code> to search in all languages
2186
     * @param stringBuilder a StringBuilder to be reused, if <code>null</code> a new StringBuilder will be instantiated and is returned
2187
     * @return the StringBuilder given a parameter or a new one if the stringBuilder parameter was null.
2188
     *
2189
     * TODO move to utiliy class !!!!!!!!
2190
     */
2191
    private StringBuilder appendLocalizedFieldQuery(String name, List<Language> languages, StringBuilder stringBuilder) {
2192

    
2193
        if(stringBuilder == null){
2194
            stringBuilder = new StringBuilder();
2195
        }
2196
        if(languages == null || languages.size() == 0){
2197
            stringBuilder.append(name + ".ALL:(%1$s) ");
2198
        } else {
2199
            for(Language lang : languages){
2200
                stringBuilder.append(name + "." + lang.getUuid().toString() + ":(%1$s) ");
2201
            }
2202
        }
2203
        return stringBuilder;
2204
    }
2205

    
2206
    @Override
2207
    public List<Synonym> createInferredSynonyms(Taxon taxon, Classification classification, SynonymType type, boolean doWithMisappliedNames){
2208
        List <Synonym> inferredSynonyms = new ArrayList<>();
2209
        List<Synonym> inferredSynonymsToBeRemoved = new ArrayList<>();
2210

    
2211
        HashMap <UUID, IZoologicalName> zooHashMap = new HashMap<>();
2212

    
2213

    
2214
        UUID nameUuid= taxon.getName().getUuid();
2215
        IZoologicalName taxonName = getZoologicalName(nameUuid, zooHashMap);
2216
        String epithetOfTaxon = null;
2217
        String infragenericEpithetOfTaxon = null;
2218
        String infraspecificEpithetOfTaxon = null;
2219
        if (taxonName.isSpecies()){
2220
             epithetOfTaxon= taxonName.getSpecificEpithet();
2221
        } else if (taxonName.isInfraGeneric()){
2222
            infragenericEpithetOfTaxon = taxonName.getInfraGenericEpithet();
2223
        } else if (taxonName.isInfraSpecific()){
2224
            infraspecificEpithetOfTaxon = taxonName.getInfraSpecificEpithet();
2225
        }
2226
        String genusOfTaxon = taxonName.getGenusOrUninomial();
2227
        Set<TaxonNode> nodes = taxon.getTaxonNodes();
2228
        List<String> taxonNames = new ArrayList<>();
2229

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

    
2234
            if (node.getClassification().equals(classification)){
2235
                if (!node.isTopmostNode()){
2236
                    TaxonNode parent = node.getParent();
2237
                    parent = CdmBase.deproxy(parent);
2238
                    TaxonName parentName =  parent.getTaxon().getName();
2239
                    IZoologicalName zooParentName = CdmBase.deproxy(parentName);
2240
                    Taxon parentTaxon = CdmBase.deproxy(parent.getTaxon());
2241
                    Rank rankOfTaxon = taxonName.getRank();
2242

    
2243

    
2244
                    //create inferred synonyms for species, subspecies
2245
                    if ((parentName.isGenus() || parentName.isSpecies() || parentName.getRank().equals(Rank.SUBGENUS())) ){
2246

    
2247
                        Synonym inferredEpithet = null;
2248
                        Synonym inferredGenus = null;
2249
                        Synonym potentialCombination = null;
2250

    
2251
                        List<String> propertyPaths = new ArrayList<>();
2252
                        propertyPaths.add("synonym");
2253
                        propertyPaths.add("synonym.name");
2254
                        List<OrderHint> orderHintsSynonyms = new ArrayList<>();
2255
                        orderHintsSynonyms.add(new OrderHint("titleCache", SortOrder.ASCENDING));
2256

    
2257
                        List<Synonym> synonyMsOfParent = dao.getSynonyms(parentTaxon, SynonymType.HETEROTYPIC_SYNONYM_OF(), null, null,orderHintsSynonyms,propertyPaths);
2258
                        List<Synonym> synonymsOfTaxon= dao.getSynonyms(taxon, SynonymType.HETEROTYPIC_SYNONYM_OF(),
2259
                                null, null,orderHintsSynonyms,propertyPaths);
2260

    
2261
                        List<TaxonRelationship> taxonRelListParent = new ArrayList<>();
2262
                        List<TaxonRelationship> taxonRelListTaxon = new ArrayList<>();
2263
                        if (doWithMisappliedNames){
2264
                            List<OrderHint> orderHintsMisapplied = new ArrayList<>();
2265
                            orderHintsMisapplied.add(new OrderHint("relatedFrom.titleCache", SortOrder.ASCENDING));
2266
                            taxonRelListParent = dao.getTaxonRelationships(parentTaxon, TaxonRelationshipType.MISAPPLIED_NAME_FOR(),
2267
                                    null, null, orderHintsMisapplied, propertyPaths, Direction.relatedTo);
2268
                            taxonRelListTaxon = dao.getTaxonRelationships(taxon, TaxonRelationshipType.MISAPPLIED_NAME_FOR(),
2269
                                    null, null, orderHintsMisapplied, propertyPaths, Direction.relatedTo);
2270
                        }
2271

    
2272
                        if (type.equals(SynonymType.INFERRED_EPITHET_OF())){
2273
                            for (Synonym synonymRelationOfParent:synonyMsOfParent){
2274

    
2275
                                inferredEpithet = createInferredEpithets(taxon,
2276
                                        zooHashMap, taxonName, epithetOfTaxon,
2277
                                        infragenericEpithetOfTaxon,
2278
                                        infraspecificEpithetOfTaxon,
2279
                                        taxonNames, parentName,
2280
                                        synonymRelationOfParent);
2281

    
2282
                                inferredSynonyms.add(inferredEpithet);
2283
                                zooHashMap.put(inferredEpithet.getName().getUuid(), inferredEpithet.getName());
2284
                                taxonNames.add(inferredEpithet.getName().getNameCache());
2285
                            }
2286

    
2287
                            if (doWithMisappliedNames){
2288

    
2289
                                for (TaxonRelationship taxonRelationship: taxonRelListParent){
2290
                                     Taxon misappliedName = taxonRelationship.getFromTaxon();
2291

    
2292
                                     inferredEpithet = createInferredEpithets(taxon,
2293
                                             zooHashMap, taxonName, epithetOfTaxon,
2294
                                             infragenericEpithetOfTaxon,
2295
                                             infraspecificEpithetOfTaxon,
2296
                                             taxonNames, parentName,
2297
                                             misappliedName);
2298

    
2299
                                    inferredSynonyms.add(inferredEpithet);
2300
                                    zooHashMap.put(inferredEpithet.getName().getUuid(), inferredEpithet.getName());
2301
                                    taxonNames.add(inferredEpithet.getName().getNameCache());
2302
                                }
2303
                            }
2304

    
2305
                            if (!taxonNames.isEmpty()){
2306
                            List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2307
                            IZoologicalName name;
2308
                            if (!synNotInCDM.isEmpty()){
2309
                                inferredSynonymsToBeRemoved.clear();
2310

    
2311
                                for (Synonym syn :inferredSynonyms){
2312
                                    name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2313
                                    if (!synNotInCDM.contains(name.getNameCache())){
2314
                                        inferredSynonymsToBeRemoved.add(syn);
2315
                                    }
2316
                                }
2317

    
2318
                                // Remove identified Synonyms from inferredSynonyms
2319
                                for (Synonym synonym : inferredSynonymsToBeRemoved) {
2320
                                    inferredSynonyms.remove(synonym);
2321
                                }
2322
                            }
2323
                        }
2324

    
2325
                    }else if (type.equals(SynonymType.INFERRED_GENUS_OF())){
2326

    
2327
                        for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2328

    
2329
                            inferredGenus = createInferredGenus(taxon,
2330
                                    zooHashMap, taxonName, epithetOfTaxon,
2331
                                    genusOfTaxon, taxonNames, zooParentName, synonymRelationOfTaxon);
2332

    
2333
                            inferredSynonyms.add(inferredGenus);
2334
                            zooHashMap.put(inferredGenus.getName().getUuid(), inferredGenus.getName());
2335
                            taxonNames.add(inferredGenus.getName().getNameCache());
2336
                        }
2337

    
2338
                        if (doWithMisappliedNames){
2339

    
2340
                            for (TaxonRelationship taxonRelationship: taxonRelListTaxon){
2341
                                Taxon misappliedName = taxonRelationship.getFromTaxon();
2342
                                inferredGenus = createInferredGenus(taxon, zooHashMap, taxonName, infraspecificEpithetOfTaxon, genusOfTaxon, taxonNames, zooParentName,  misappliedName);
2343

    
2344
                                inferredSynonyms.add(inferredGenus);
2345
                                zooHashMap.put(inferredGenus.getName().getUuid(), inferredGenus.getName());
2346
                                 taxonNames.add(inferredGenus.getName().getNameCache());
2347
                            }
2348
                        }
2349

    
2350

    
2351
                        if (!taxonNames.isEmpty()){
2352
                            List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2353
                            IZoologicalName name;
2354
                            if (!synNotInCDM.isEmpty()){
2355
                                inferredSynonymsToBeRemoved.clear();
2356

    
2357
                                for (Synonym syn :inferredSynonyms){
2358
                                    name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2359
                                    if (!synNotInCDM.contains(name.getNameCache())){
2360
                                        inferredSynonymsToBeRemoved.add(syn);
2361
                                    }
2362
                                }
2363

    
2364
                                // Remove identified Synonyms from inferredSynonyms
2365
                                for (Synonym synonym : inferredSynonymsToBeRemoved) {
2366
                                    inferredSynonyms.remove(synonym);
2367
                                }
2368
                            }
2369
                        }
2370

    
2371
                    }else if (type.equals(SynonymType.POTENTIAL_COMBINATION_OF())){
2372

    
2373
                        Reference sourceReference = null; // TODO: Determination of sourceReference is redundant
2374
                        IZoologicalName inferredSynName;
2375
                        //for all synonyms of the parent...
2376
                        for (Synonym synonymRelationOfParent:synonyMsOfParent){
2377
                            TaxonName synName;
2378
                            HibernateProxyHelper.deproxy(synonymRelationOfParent);
2379

    
2380
                            synName = synonymRelationOfParent.getName();
2381

    
2382
                            // Set the sourceReference
2383
                            sourceReference = synonymRelationOfParent.getSec();
2384

    
2385
                            // Determine the idInSource
2386
                            String idInSourceParent = getIdInSource(synonymRelationOfParent);
2387

    
2388
                            IZoologicalName parentSynZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2389
                            String synParentGenus = parentSynZooName.getGenusOrUninomial();
2390
                            String synParentInfragenericName = null;
2391
                            String synParentSpecificEpithet = null;
2392

    
2393
                            if (parentSynZooName.isInfraGeneric()){
2394
                                synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2395
                            }
2396
                            if (parentSynZooName.isSpecies()){
2397
                                synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2398
                            }
2399

    
2400
                           /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2401
                                synonymsGenus.put(synGenusName, idInSource);
2402
                            }*/
2403

    
2404
                            //for all synonyms of the taxon
2405

    
2406
                            for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2407

    
2408
                                IZoologicalName zooSynName = getZoologicalName(synonymRelationOfTaxon.getName().getUuid(), zooHashMap);
2409
                                potentialCombination = createPotentialCombination(idInSourceParent, parentSynZooName, zooSynName,
2410
                                        synParentGenus,
2411
                                        synParentInfragenericName,
2412
                                        synParentSpecificEpithet, synonymRelationOfTaxon, zooHashMap);
2413

    
2414
                                taxon.addSynonym(potentialCombination, SynonymType.POTENTIAL_COMBINATION_OF());
2415
                                inferredSynonyms.add(potentialCombination);
2416
                                zooHashMap.put(potentialCombination.getName().getUuid(), potentialCombination.getName());
2417
                                 taxonNames.add(potentialCombination.getName().getNameCache());
2418

    
2419
                            }
2420

    
2421
                        }
2422

    
2423
                        if (doWithMisappliedNames){
2424

    
2425
                            for (TaxonRelationship parentRelationship: taxonRelListParent){
2426

    
2427
                                TaxonName misappliedParentName;
2428

    
2429
                                Taxon misappliedParent = parentRelationship.getFromTaxon();
2430
                                misappliedParentName = misappliedParent.getName();
2431

    
2432
                                HibernateProxyHelper.deproxy(misappliedParent);
2433

    
2434
                                // Set the sourceReference
2435
                                sourceReference = misappliedParent.getSec();
2436

    
2437
                                // Determine the idInSource
2438
                                String idInSourceParent = getIdInSource(misappliedParent);
2439

    
2440
                                IZoologicalName parentSynZooName = getZoologicalName(misappliedParentName.getUuid(), zooHashMap);
2441
                                String synParentGenus = parentSynZooName.getGenusOrUninomial();
2442
                                String synParentInfragenericName = null;
2443
                                String synParentSpecificEpithet = null;
2444

    
2445
                                if (parentSynZooName.isInfraGeneric()){
2446
                                    synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2447
                                }
2448
                                if (parentSynZooName.isSpecies()){
2449
                                    synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2450
                                }
2451

    
2452

    
2453
                                for (TaxonRelationship taxonRelationship: taxonRelListTaxon){
2454
                                    Taxon misappliedName = taxonRelationship.getFromTaxon();
2455
                                    IZoologicalName zooMisappliedName = getZoologicalName(misappliedName.getName().getUuid(), zooHashMap);
2456
                                    potentialCombination = createPotentialCombination(
2457
                                            idInSourceParent, parentSynZooName, zooMisappliedName,
2458
                                            synParentGenus,
2459
                                            synParentInfragenericName,
2460
                                            synParentSpecificEpithet, misappliedName, zooHashMap);
2461

    
2462

    
2463
                                    taxon.addSynonym(potentialCombination, SynonymType.POTENTIAL_COMBINATION_OF());
2464
                                    inferredSynonyms.add(potentialCombination);
2465
                                    zooHashMap.put(potentialCombination.getName().getUuid(), potentialCombination.getName());
2466
                                     taxonNames.add(potentialCombination.getName().getNameCache());
2467
                                }
2468
                            }
2469
                        }
2470

    
2471
                        if (!taxonNames.isEmpty()){
2472
                            List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2473
                            IZoologicalName name;
2474
                            if (!synNotInCDM.isEmpty()){
2475
                                inferredSynonymsToBeRemoved.clear();
2476
                                for (Synonym syn :inferredSynonyms){
2477
                                    try{
2478
                                        name = syn.getName();
2479
                                    }catch (ClassCastException e){
2480
                                        name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2481
                                    }
2482
                                    if (!synNotInCDM.contains(name.getNameCache())){
2483
                                        inferredSynonymsToBeRemoved.add(syn);
2484
                                    }
2485
                                 }
2486
                                // Remove identified Synonyms from inferredSynonyms
2487
                                for (Synonym synonym : inferredSynonymsToBeRemoved) {
2488
                                    inferredSynonyms.remove(synonym);
2489
                                }
2490
                            }
2491
                         }
2492
                        }
2493
                    }else {
2494
                        logger.info("The synonym type is not defined.");
2495
                        return inferredSynonyms;
2496
                    }
2497
                }
2498
            }
2499

    
2500
        }
2501

    
2502
        return inferredSynonyms;
2503
    }
2504

    
2505
    private Synonym createPotentialCombination(String idInSourceParent,
2506
            IZoologicalName parentSynZooName, 	IZoologicalName zooSynName, String synParentGenus,
2507
            String synParentInfragenericName, String synParentSpecificEpithet,
2508
            TaxonBase<?> syn, Map<UUID, IZoologicalName> zooHashMap) {
2509
        Synonym potentialCombination;
2510
        Reference sourceReference;
2511
        IZoologicalName inferredSynName;
2512
        HibernateProxyHelper.deproxy(syn);
2513

    
2514
        // Set sourceReference
2515
        sourceReference = syn.getSec();
2516
        if (sourceReference == null){
2517
            logger.warn("The synonym has no sec reference because it is a misapplied name! Take the sec reference of taxon");
2518
            //TODO:Remove
2519
            if (!parentSynZooName.getTaxa().isEmpty()){
2520
                TaxonBase<?> taxon = parentSynZooName.getTaxa().iterator().next();
2521

    
2522
                sourceReference = taxon.getSec();
2523
            }
2524
        }
2525
        String synTaxonSpecificEpithet = zooSynName.getSpecificEpithet();
2526

    
2527
        String synTaxonInfraSpecificName= null;
2528

    
2529
        if (parentSynZooName.isSpecies()){
2530
            synTaxonInfraSpecificName = zooSynName.getInfraSpecificEpithet();
2531
        }
2532

    
2533
        /*if (epithetName != null && !synonymsEpithet.contains(epithetName)){
2534
            synonymsEpithet.add(epithetName);
2535
        }*/
2536

    
2537
        //create potential combinations...
2538
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(syn.getName().getRank());
2539

    
2540
        inferredSynName.setGenusOrUninomial(synParentGenus);
2541
        if (zooSynName.isSpecies()){
2542
              inferredSynName.setSpecificEpithet(synTaxonSpecificEpithet);
2543
              if (parentSynZooName.isInfraGeneric()){
2544
                  inferredSynName.setInfraGenericEpithet(synParentInfragenericName);
2545
              }
2546
        }
2547
        if (zooSynName.isInfraSpecific()){
2548
            inferredSynName.setSpecificEpithet(synParentSpecificEpithet);
2549
            inferredSynName.setInfraSpecificEpithet(synTaxonInfraSpecificName);
2550
        }
2551
        if (parentSynZooName.isInfraGeneric()){
2552
            inferredSynName.setInfraGenericEpithet(synParentInfragenericName);
2553
        }
2554

    
2555

    
2556
        potentialCombination = Synonym.NewInstance(inferredSynName, null);
2557

    
2558
        // Set the sourceReference
2559
        potentialCombination.setSec(sourceReference);
2560

    
2561

    
2562
        // Determine the idInSource
2563
        String idInSourceSyn= getIdInSource(syn);
2564

    
2565
        if (idInSourceParent != null && idInSourceSyn != null) {
2566
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2567
            inferredSynName.addSource(originalSource);
2568
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2569
            potentialCombination.addSource(originalSource);
2570
        }
2571

    
2572
        return potentialCombination;
2573
    }
2574

    
2575
    private Synonym createInferredGenus(Taxon taxon,
2576
            Map<UUID, IZoologicalName> zooHashMap, IZoologicalName taxonName,
2577
            String epithetOfTaxon, String genusOfTaxon,
2578
            List<String> taxonNames, IZoologicalName zooParentName,
2579
            TaxonBase syn) {
2580

    
2581
        Synonym inferredGenus;
2582
        TaxonName synName;
2583
        IZoologicalName inferredSynName;
2584
        synName =syn.getName();
2585
        HibernateProxyHelper.deproxy(syn);
2586

    
2587
        // Determine the idInSource
2588
        String idInSourceSyn = getIdInSource(syn);
2589
        String idInSourceTaxon = getIdInSource(taxon);
2590
        // Determine the sourceReference
2591
        Reference sourceReference = syn.getSec();
2592

    
2593
        //logger.warn(sourceReference.getTitleCache());
2594

    
2595
        synName = syn.getName();
2596
        IZoologicalName synZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2597
        String synSpeciesEpithetName = synZooName.getSpecificEpithet();
2598
                     /* if (synonymsEpithet != null && !synonymsEpithet.contains(synSpeciesEpithetName)){
2599
            synonymsEpithet.add(synSpeciesEpithetName);
2600
        }*/
2601

    
2602
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2603
        //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...
2604

    
2605

    
2606
        inferredSynName.setGenusOrUninomial(genusOfTaxon);
2607
        if (zooParentName.isInfraGeneric()){
2608
            inferredSynName.setInfraGenericEpithet(zooParentName.getInfraGenericEpithet());
2609
        }
2610

    
2611
        if (taxonName.isSpecies()){
2612
            inferredSynName.setSpecificEpithet(synSpeciesEpithetName);
2613
        }
2614
        if (taxonName.isInfraSpecific()){
2615
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2616
            inferredSynName.setInfraSpecificEpithet(synZooName.getInfraGenericEpithet());
2617
        }
2618

    
2619

    
2620
        inferredGenus = Synonym.NewInstance(inferredSynName, null);
2621

    
2622
        // Set the sourceReference
2623
        inferredGenus.setSec(sourceReference);
2624

    
2625
        // Add the original source
2626
        if (idInSourceSyn != null && idInSourceTaxon != null) {
2627
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2628
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2629
            inferredGenus.addSource(originalSource);
2630

    
2631
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2632
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2633
            inferredSynName.addSource(originalSource);
2634
            originalSource = null;
2635

    
2636
        }else{
2637
            logger.error("There is an idInSource missing: " + idInSourceSyn + " of Synonym or " + idInSourceTaxon + " of Taxon");
2638
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2639
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2640
            inferredGenus.addSource(originalSource);
2641

    
2642
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2643
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2644
            inferredSynName.addSource(originalSource);
2645
            originalSource = null;
2646
        }
2647

    
2648
        taxon.addSynonym(inferredGenus, SynonymType.INFERRED_GENUS_OF());
2649

    
2650
        return inferredGenus;
2651
    }
2652

    
2653
    private Synonym createInferredEpithets(Taxon taxon,
2654
            Map<UUID, IZoologicalName> zooHashMap, IZoologicalName taxonName,
2655
            String epithetOfTaxon, String infragenericEpithetOfTaxon,
2656
            String infraspecificEpithetOfTaxon, List<String> taxonNames,
2657
            TaxonName parentName, TaxonBase<?> syn) {
2658

    
2659
        Synonym inferredEpithet;
2660
        TaxonName synName;
2661
        IZoologicalName inferredSynName;
2662
        HibernateProxyHelper.deproxy(syn);
2663

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

    
2670
        if (sourceReference == null){
2671
             logger.warn("The synonym has no sec reference because it is a misapplied name! Take the sec reference of taxon" + taxon.getSec());
2672
             sourceReference = taxon.getSec();
2673
        }
2674

    
2675
        synName = syn.getName();
2676
        IZoologicalName zooSynName = getZoologicalName(synName.getUuid(), zooHashMap);
2677
        String synGenusName = zooSynName.getGenusOrUninomial();
2678
        String synInfraGenericEpithet = null;
2679
        String synSpecificEpithet = null;
2680

    
2681
        if (zooSynName.getInfraGenericEpithet() != null){
2682
            synInfraGenericEpithet = zooSynName.getInfraGenericEpithet();
2683
        }
2684

    
2685
        if (zooSynName.isInfraSpecific()){
2686
            synSpecificEpithet = zooSynName.getSpecificEpithet();
2687
        }
2688

    
2689
                     /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2690
            synonymsGenus.put(synGenusName, idInSource);
2691
        }*/
2692

    
2693
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2694

    
2695
        // DEBUG TODO: for subgenus or subspecies the infrageneric or infraspecific epithet should be used!!!
2696
        if (epithetOfTaxon == null && infragenericEpithetOfTaxon == null && infraspecificEpithetOfTaxon == null) {
2697
            logger.error("This specificEpithet is NULL" + taxon.getTitleCache());
2698
        }
2699
        inferredSynName.setGenusOrUninomial(synGenusName);
2700

    
2701
        if (parentName.isInfraGeneric()){
2702
            inferredSynName.setInfraGenericEpithet(synInfraGenericEpithet);
2703
        }
2704
        if (taxonName.isSpecies()){
2705
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2706
        }else if (taxonName.isInfraSpecific()){
2707
            inferredSynName.setSpecificEpithet(synSpecificEpithet);
2708
            inferredSynName.setInfraSpecificEpithet(infraspecificEpithetOfTaxon);
2709
        }
2710

    
2711
        inferredEpithet = Synonym.NewInstance(inferredSynName, null);
2712

    
2713
        // Set the sourceReference
2714
        inferredEpithet.setSec(sourceReference);
2715

    
2716
        /* Add the original source
2717
        if (idInSource != null) {
2718
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(idInSource, "InferredEpithetOf", syn.getSec(), null);
2719

    
2720
            // Add the citation
2721
            Reference citation = getCitation(syn);
2722
            if (citation != null) {
2723
                originalSource.setCitation(citation);
2724
                inferredEpithet.addSource(originalSource);
2725
            }
2726
        }*/
2727
        String taxonId = idInSourceTaxon+ "; " + idInSourceSyn;
2728

    
2729

    
2730
        IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2731
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2732

    
2733
        inferredEpithet.addSource(originalSource);
2734

    
2735
        originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2736
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2737

    
2738
        inferredSynName.addSource(originalSource);
2739

    
2740

    
2741

    
2742
        taxon.addSynonym(inferredEpithet, SynonymType.INFERRED_EPITHET_OF());
2743

    
2744
        return inferredEpithet;
2745
    }
2746

    
2747
    /**
2748
     * Returns an existing IZoologicalName or extends an internal hashmap if it does not exist.
2749
     * Very likely only useful for createInferredSynonyms().
2750
     * @param uuid
2751
     * @param zooHashMap
2752
     * @return
2753
     */
2754
    private IZoologicalName getZoologicalName(UUID uuid, Map <UUID, IZoologicalName> zooHashMap) {
2755
        IZoologicalName taxonName =nameDao.findZoologicalNameByUUID(uuid);
2756
        if (taxonName == null) {
2757
            taxonName = zooHashMap.get(uuid);
2758
        }
2759
        return taxonName;
2760
    }
2761

    
2762
    /**
2763
     * Returns the idInSource for a given Synonym.
2764
     * @param syn
2765
     */
2766
    private String getIdInSource(TaxonBase<?> taxonBase) {
2767
        String idInSource = null;
2768
        Set<IdentifiableSource> sources = taxonBase.getSources();
2769
        if (sources.size() == 1) {
2770
            IdentifiableSource source = sources.iterator().next();
2771
            if (source != null) {
2772
                idInSource  = source.getIdInSource();
2773
            }
2774
        } else if (sources.size() > 1) {
2775
            int count = 1;
2776
            idInSource = "";
2777
            for (IdentifiableSource source : sources) {
2778
                idInSource += source.getIdInSource();
2779
                if (count < sources.size()) {
2780
                    idInSource += "; ";
2781
                }
2782
                count++;
2783
            }
2784
        } else if (sources.size() == 0){
2785
            logger.warn("No idInSource for TaxonBase " + taxonBase.getUuid() + " - " + taxonBase.getTitleCache());
2786
        }
2787

    
2788

    
2789
        return idInSource;
2790
    }
2791

    
2792

    
2793
    /**
2794
     * Returns the citation for a given Synonym.
2795
     * @param syn
2796
     */
2797
    private Reference getCitation(Synonym syn) {
2798
        Reference citation = null;
2799
        Set<IdentifiableSource> sources = syn.getSources();
2800
        if (sources.size() == 1) {
2801
            IdentifiableSource source = sources.iterator().next();
2802
            if (source != null) {
2803
                citation = source.getCitation();
2804
            }
2805
        } else if (sources.size() > 1) {
2806
            logger.warn("This Synonym has more than one source: " + syn.getUuid() + " (" + syn.getTitleCache() +")");
2807
        }
2808

    
2809
        return citation;
2810
    }
2811

    
2812
    @Override
2813
    public List<Synonym>  createAllInferredSynonyms(Taxon taxon, Classification tree, boolean doWithMisappliedNames){
2814
        List <Synonym> inferredSynonyms = new ArrayList<>();
2815

    
2816
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.INFERRED_EPITHET_OF(), doWithMisappliedNames));
2817
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.INFERRED_GENUS_OF(), doWithMisappliedNames));
2818
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.POTENTIAL_COMBINATION_OF(), doWithMisappliedNames));
2819

    
2820
        return inferredSynonyms;
2821
    }
2822

    
2823
    @Override
2824
    public List<Classification> listClassifications(TaxonBase taxonBase, Integer limit, Integer start, List<String> propertyPaths) {
2825

    
2826
        // TODO quickly implemented, create according dao !!!!
2827
        Set<TaxonNode> nodes = new HashSet<>();
2828
        Set<Classification> classifications = new HashSet<>();
2829
        List<Classification> list = new ArrayList<>();
2830

    
2831
        if (taxonBase == null) {
2832
            return list;
2833
        }
2834

    
2835
        taxonBase = load(taxonBase.getUuid());
2836

    
2837
        if (taxonBase instanceof Taxon) {
2838
            nodes.addAll(((Taxon)taxonBase).getTaxonNodes());
2839
        } else {
2840
            Taxon taxon = ((Synonym)taxonBase).getAcceptedTaxon();
2841
            if (taxon != null){
2842
                nodes.addAll(taxon.getTaxonNodes());
2843
            }
2844
        }
2845
        for (TaxonNode node : nodes) {
2846
            classifications.add(node.getClassification());
2847
        }
2848
        list.addAll(classifications);
2849
        return list;
2850
    }
2851

    
2852
    @Override
2853
    @Transactional(readOnly = false)
2854
    public UpdateResult changeRelatedTaxonToSynonym(UUID fromTaxonUuid,
2855
            UUID toTaxonUuid,
2856
            TaxonRelationshipType oldRelationshipType,
2857
            SynonymType synonymType) throws DataChangeNoRollbackException {
2858
        UpdateResult result = new UpdateResult();
2859
        Taxon fromTaxon = (Taxon) dao.load(fromTaxonUuid);
2860
        Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
2861
        result = changeRelatedTaxonToSynonym(fromTaxon, toTaxon, oldRelationshipType, synonymType);
2862

    
2863
        result.addUpdatedObject(fromTaxon);
2864
        result.addUpdatedObject(toTaxon);
2865
        result.addUpdatedObject(result.getCdmEntity());
2866

    
2867
        return result;
2868
    }
2869

    
2870
    @Override
2871
    @Transactional(readOnly = false)
2872
    public UpdateResult changeRelatedTaxonToSynonym(Taxon fromTaxon, Taxon toTaxon, TaxonRelationshipType oldRelationshipType,
2873
            SynonymType synonymType) throws DataChangeNoRollbackException {
2874

    
2875
        UpdateResult result = new UpdateResult();
2876
        // Create new synonym using concept name
2877
        TaxonName synonymName = fromTaxon.getName();
2878

    
2879
        // Remove concept relation from taxon
2880
        toTaxon.removeTaxon(fromTaxon, oldRelationshipType);
2881

    
2882
        // Create a new synonym for the taxon
2883
        Synonym synonym;
2884
        if (synonymType != null
2885
                && synonymType.equals(SynonymType.HOMOTYPIC_SYNONYM_OF())){
2886
            synonym = Synonym.NewInstance(synonymName, fromTaxon.getSec());
2887
            toTaxon.addHomotypicSynonym(synonym);
2888
        } else{
2889
            synonym = toTaxon.addHeterotypicSynonymName(synonymName);
2890
        }
2891

    
2892
        this.saveOrUpdate(toTaxon);
2893
        //TODO: configurator and classification
2894
        TaxonDeletionConfigurator config = new TaxonDeletionConfigurator();
2895
        config.setDeleteNameIfPossible(false);
2896
        result.includeResult(this.deleteTaxon(fromTaxon.getUuid(), config, null));
2897
        result.setCdmEntity(synonym);
2898
        result.addUpdatedObject(toTaxon);
2899
        result.addUpdatedObject(synonym);
2900
        return result;
2901
    }
2902

    
2903
    @Override
2904
    public DeleteResult isDeletable(UUID taxonBaseUuid, DeleteConfiguratorBase config){
2905
        DeleteResult result = new DeleteResult();
2906
        TaxonBase<?> taxonBase = load(taxonBaseUuid);
2907
        Set<CdmBase> references = commonService.getReferencingObjectsForDeletion(taxonBase);
2908
        if (taxonBase instanceof Taxon){
2909
            TaxonDeletionConfigurator taxonConfig = (TaxonDeletionConfigurator) config;
2910
            result = isDeletableForTaxon(references, taxonConfig);
2911
        }else{
2912
            SynonymDeletionConfigurator synonymConfig = (SynonymDeletionConfigurator) config;
2913
            result = isDeletableForSynonym(references, synonymConfig);
2914
        }
2915
        return result;
2916
    }
2917

    
2918
    private DeleteResult isDeletableForSynonym(Set<CdmBase> references, SynonymDeletionConfigurator config){
2919
        String message;
2920
        DeleteResult result = new DeleteResult();
2921
        for (CdmBase ref: references){
2922
            if (!(ref instanceof Taxon || ref instanceof TaxonName )){
2923
                message = "The Synonym can't be deleted as long as it is referenced by " + ref.getClass().getSimpleName() + " with id "+ ref.getId();
2924
                result.addException(new ReferencedObjectUndeletableException(message));
2925
                result.addRelatedObject(ref);
2926
                result.setAbort();
2927
            }
2928
        }
2929

    
2930
        return result;
2931
    }
2932

    
2933
    private DeleteResult isDeletableForTaxon(Set<CdmBase> references, TaxonDeletionConfigurator config){
2934
        String message = null;
2935
        DeleteResult result = new DeleteResult();
2936
        for (CdmBase ref: references){
2937
            if (!(ref instanceof TaxonName)){
2938
            	message = null;
2939
                if (!config.isDeleteSynonymRelations() && (ref instanceof Synonym)){
2940
                    message = "The taxon can't be deleted as long as it has synonyms.";
2941
                }
2942
                if (!config.isDeleteDescriptions() && (ref instanceof DescriptionBase)){
2943
                    message = "The taxon can't be deleted as long as it has factual data.";
2944
                }
2945

    
2946
                if (!config.isDeleteTaxonNodes() && (ref instanceof TaxonNode)){
2947
                    message = "The taxon can't be deleted as long as it belongs to a taxon node.";
2948
                }
2949
                if (!config.isDeleteTaxonRelationships() && (ref instanceof TaxonRelationship)){
2950
                    if (!config.isDeleteMisappliedNamesAndInvalidDesignations() &&
2951
                            (((TaxonRelationship)ref).getType().isMisappliedNameOrInvalidDesignation())){
2952
                        message = "The taxon can't be deleted as long as it has misapplied names or invalid designations.";
2953
                    } else{
2954
                        message = "The taxon can't be deleted as long as it belongs to taxon relationship.";
2955
                    }
2956
                }
2957
                if (ref instanceof PolytomousKeyNode){
2958
                    message = "The taxon can't be deleted as long as it is referenced by a polytomous key node.";
2959
                }
2960

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

    
2965

    
2966
               /* //PolytomousKeyNode
2967
                if (referencingObject.isInstanceOf(PolytomousKeyNode.class)){
2968
                    String message = "Taxon" + taxon.getTitleCache() + " can't be deleted as it is used in polytomous key node";
2969
                    return message;
2970
                }*/
2971

    
2972
                //TaxonInteraction
2973
                if (ref.isInstanceOf(TaxonInteraction.class)){
2974
                    message = "Taxon can't be deleted as it is used in taxonInteraction#taxon2";
2975
                }
2976

    
2977
              //TaxonInteraction
2978
                if (ref.isInstanceOf(DeterminationEvent.class)){
2979
                    message = "Taxon can't be deleted as it is used in a determination event";
2980
                }
2981
            }
2982
            if (message != null){
2983
	            result.addException(new ReferencedObjectUndeletableException(message));
2984
	            result.addRelatedObject(ref);
2985
	            result.setAbort();
2986
            }
2987
        }
2988

    
2989
        return result;
2990
    }
2991

    
2992
    @Override
2993
    public IncludedTaxaDTO listIncludedTaxa(UUID taxonUuid, IncludedTaxonConfiguration config) {
2994
        IncludedTaxaDTO result = new IncludedTaxaDTO(taxonUuid);
2995

    
2996
        //preliminary implementation
2997

    
2998
        Set<Taxon> taxa = new HashSet<>();
2999
        TaxonBase<?> taxonBase = find(taxonUuid);
3000
        if (taxonBase == null){
3001
            return new IncludedTaxaDTO();
3002
        }else if (taxonBase.isInstanceOf(Taxon.class)){
3003
            Taxon taxon = CdmBase.deproxy(taxonBase, Taxon.class);
3004
            taxa.add(taxon);
3005
        }else if (taxonBase.isInstanceOf(Synonym.class)){
3006
            //TODO partial synonyms ??
3007
            //TODO synonyms in general
3008
            Synonym syn = CdmBase.deproxy(taxonBase, Synonym.class);
3009
            taxa.add(syn.getAcceptedTaxon());
3010
        }else{
3011
            throw new IllegalArgumentException("Unhandled class " + taxonBase.getClass().getSimpleName());
3012
        }
3013

    
3014
        Set<Taxon> related = makeRelatedIncluded(taxa, result, config);
3015
        int i = 0;
3016
        while((! related.isEmpty()) && i++ < 100){  //to avoid
3017
             related = makeRelatedIncluded(related, result, config);
3018
        }
3019

    
3020
        return result;
3021
    }
3022

    
3023
    /**
3024
     * Computes all children and conceptually congruent and included taxa and adds them to the existingTaxa
3025
     * data structure.
3026
     * @return the set of conceptually related taxa for further use
3027
     */
3028
    /**
3029
     * @param uncheckedTaxa
3030
     * @param existingTaxa
3031
     * @param config
3032
     * @return
3033
     */
3034
    private Set<Taxon> makeRelatedIncluded(Set<Taxon> uncheckedTaxa, IncludedTaxaDTO existingTaxa, IncludedTaxonConfiguration config) {
3035

    
3036
        //children
3037
        Set<TaxonNode> taxonNodes = new HashSet<>();
3038
        for (Taxon taxon: uncheckedTaxa){
3039
            taxonNodes.addAll(taxon.getTaxonNodes());
3040
        }
3041

    
3042
        Set<Taxon> children = new HashSet<>();
3043
        if (! config.onlyCongruent){
3044
            for (TaxonNode node: taxonNodes){
3045
                List<TaxonNode> childNodes = nodeService.loadChildNodesOfTaxonNode(node, null, true, null);
3046
                for (TaxonNode child : childNodes){
3047
                    children.add(child.getTaxon());
3048
                }
3049
            }
3050
            children.remove(null);  // just to be on the save side
3051
        }
3052

    
3053
        Iterator<Taxon> it = children.iterator();
3054
        while(it.hasNext()){
3055
            UUID uuid = it.next().getUuid();
3056
            if (existingTaxa.contains(uuid)){
3057
                it.remove();
3058
            }else{
3059
                existingTaxa.addIncludedTaxon(uuid, new ArrayList<>(), false);
3060
            }
3061
        }
3062

    
3063
        //concept relations
3064
        Set<Taxon> uncheckedAndChildren = new HashSet<>(uncheckedTaxa);
3065
        uncheckedAndChildren.addAll(children);
3066

    
3067
        Set<Taxon> relatedTaxa = makeConceptIncludedTaxa(uncheckedAndChildren, existingTaxa, config);
3068

    
3069

    
3070
        Set<Taxon> result = new HashSet<>(relatedTaxa);
3071
        return result;
3072
    }
3073

    
3074
    /**
3075
     * Computes all conceptually congruent or included taxa and adds them to the existingTaxa data structure.
3076
     * @return the set of these computed taxa
3077
     */
3078
    private Set<Taxon> makeConceptIncludedTaxa(Set<Taxon> unchecked, IncludedTaxaDTO existingTaxa, IncludedTaxonConfiguration config) {
3079
        Set<Taxon> result = new HashSet<>();
3080

    
3081
        for (Taxon taxon : unchecked){
3082
            Set<TaxonRelationship> fromRelations = taxon.getRelationsFromThisTaxon();
3083
            Set<TaxonRelationship> toRelations = taxon.getRelationsToThisTaxon();
3084

    
3085
            for (TaxonRelationship fromRel : fromRelations){
3086
                if (config.includeDoubtful == false && fromRel.isDoubtful()){
3087
                    continue;
3088
                }
3089
                if (fromRel.getType().equals(TaxonRelationshipType.CONGRUENT_TO()) ||
3090
                        !config.onlyCongruent && fromRel.getType().equals(TaxonRelationshipType.INCLUDES()) ||
3091
                        !config.onlyCongruent && fromRel.getType().equals(TaxonRelationshipType.CONGRUENT_OR_INCLUDES())
3092
                        ){
3093
                    result.add(fromRel.getToTaxon());
3094
                }
3095
            }
3096

    
3097
            for (TaxonRelationship toRel : toRelations){
3098
                if (config.includeDoubtful == false && toRel.isDoubtful()){
3099
                    continue;
3100
                }
3101
                if (toRel.getType().equals(TaxonRelationshipType.CONGRUENT_TO())){
3102
                    result.add(toRel.getFromTaxon());
3103
                }
3104
            }
3105
        }
3106

    
3107
        Iterator<Taxon> it = result.iterator();
3108
        while(it.hasNext()){
3109
            UUID uuid = it.next().getUuid();
3110
            if (existingTaxa.contains(uuid)){
3111
                it.remove();
3112
            }else{
3113
                existingTaxa.addIncludedTaxon(uuid, new ArrayList<>(), false);
3114
            }
3115
        }
3116
        return result;
3117
    }
3118

    
3119
    @Override
3120
    public List<TaxonBase> findTaxaByName(MatchingTaxonConfigurator config){
3121
        List<TaxonBase> taxonList = dao.getTaxaByName(true, config.isIncludeSynonyms(), false, false, false,
3122
                config.getTaxonNameTitle(), null, MatchMode.EXACT, null, null, 0, 0, config.getPropertyPath());
3123
        return taxonList;
3124
    }
3125

    
3126
	@Override
3127
	@Transactional(readOnly = true)
3128
	public <S extends TaxonBase> Pager<IdentifiedEntityDTO<S>> findByIdentifier(
3129
			Class<S> clazz, String identifier, DefinedTerm identifierType, TaxonNode subtreeFilter,
3130
			MatchMode matchmode, boolean includeEntity, Integer pageSize,
3131
			Integer pageNumber,	List<String> propertyPaths) {
3132
		if (subtreeFilter == null){
3133
			return findByIdentifier(clazz, identifier, identifierType, matchmode, includeEntity, pageSize, pageNumber, propertyPaths);
3134
		}
3135

    
3136
		Integer numberOfResults = dao.countByIdentifier(clazz, identifier, identifierType, subtreeFilter, matchmode);
3137
        List<Object[]> daoResults = new ArrayList<>();
3138
        if(numberOfResults > 0) { // no point checking again
3139
        	daoResults = dao.findByIdentifier(clazz, identifier, identifierType, subtreeFilter,
3140
    				matchmode, includeEntity, pageSize, pageNumber, propertyPaths);
3141
        }
3142

    
3143
        List<IdentifiedEntityDTO<S>> result = new ArrayList<>();
3144
        for (Object[] daoObj : daoResults){
3145
        	if (includeEntity){
3146
        		result.add(new IdentifiedEntityDTO<S>((DefinedTerm)daoObj[0], (String)daoObj[1], (S)daoObj[2]));
3147
        	}else{
3148
        		result.add(new IdentifiedEntityDTO<S>((DefinedTerm)daoObj[0], (String)daoObj[1], (UUID)daoObj[2], (String)daoObj[3], null));
3149
        	}
3150
        }
3151
		return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, result);
3152
	}
3153

    
3154
	@Override
3155
    @Transactional(readOnly = true)
3156
    public <S extends TaxonBase> Pager<MarkedEntityDTO<S>> findByMarker(
3157
            Class<S> clazz, MarkerType markerType, Boolean markerValue,
3158
            TaxonNode subtreeFilter, boolean includeEntity, TaxonTitleType titleType,
3159
            Integer pageSize, Integer pageNumber, List<String> propertyPaths) {
3160
        if (subtreeFilter == null){
3161
            return super.findByMarker (clazz, markerType, markerValue, includeEntity, pageSize, pageNumber, propertyPaths);
3162
        }
3163

    
3164
        Long numberOfResults = dao.countByMarker(clazz, markerType, markerValue, subtreeFilter);
3165
        List<Object[]> daoResults = new ArrayList<>();
3166
        if(numberOfResults > 0) { // no point checking again
3167
            daoResults = dao.findByMarker(clazz, markerType, markerValue, subtreeFilter,
3168
                    includeEntity, titleType, pageSize, pageNumber, propertyPaths);
3169
        }
3170

    
3171
        List<MarkedEntityDTO<S>> result = new ArrayList<>();
3172
        for (Object[] daoObj : daoResults){
3173
            if (includeEntity){
3174
                result.add(new MarkedEntityDTO<S>((MarkerType)daoObj[0], (Boolean)daoObj[1], (S)daoObj[2]));
3175
            }else{
3176
                result.add(new MarkedEntityDTO<S>((MarkerType)daoObj[0], (Boolean)daoObj[1], (UUID)daoObj[2], (String)daoObj[3]));
3177
            }
3178
        }
3179
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, result);
3180
    }
3181

    
3182
    @Override
3183
	@Transactional(readOnly = false)
3184
	public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym, UUID newTaxonUUID, boolean moveHomotypicGroup,
3185
            SynonymType newSynonymType, Reference newSecundum, String newSecundumDetail,
3186
            boolean keepSecundumIfUndefined) throws HomotypicalGroupChangeException {
3187

    
3188
	    UpdateResult result = new UpdateResult();
3189
		Taxon newTaxon = CdmBase.deproxy(dao.load(newTaxonUUID),Taxon.class);
3190
		result = moveSynonymToAnotherTaxon(oldSynonym, newTaxon, moveHomotypicGroup, newSynonymType,
3191
		        newSecundum, newSecundumDetail, keepSecundumIfUndefined);
3192

    
3193
		return result;
3194
	}
3195

    
3196
	@Override
3197
	public UpdateResult moveFactualDateToAnotherTaxon(UUID fromTaxonUuid, UUID toTaxonUuid){
3198
		UpdateResult result = new UpdateResult();
3199

    
3200
		Taxon fromTaxon = (Taxon)dao.load(fromTaxonUuid);
3201
		Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
3202
    	for(TaxonDescription description : fromTaxon.getDescriptions()){
3203
              //reload to avoid session conflicts
3204
              description = HibernateProxyHelper.deproxy(description, TaxonDescription.class);
3205

    
3206
              String moveMessage = String.format("Description moved from %s", fromTaxon);
3207
              if(description.isProtectedTitleCache()){
3208
                  String separator = "";
3209
                  if(!StringUtils.isBlank(description.getTitleCache())){
3210
                      separator = " - ";
3211
                  }
3212
                  description.setTitleCache(description.getTitleCache() + separator + moveMessage, true);
3213
              }
3214
              Annotation annotation = Annotation.NewInstance(moveMessage, Language.getDefaultLanguage());
3215
              annotation.setAnnotationType(AnnotationType.TECHNICAL());
3216
              description.addAnnotation(annotation);
3217
              toTaxon.addDescription(description);
3218
              dao.saveOrUpdate(toTaxon);
3219
              dao.saveOrUpdate(fromTaxon);
3220
              result.addUpdatedObject(toTaxon);
3221
              result.addUpdatedObject(fromTaxon);
3222

    
3223
        }
3224

    
3225
    	return result;
3226
	}
3227

    
3228
	@Override
3229
	@Transactional(readOnly = false)
3230
	public UpdateResult swapSynonymAndAcceptedTaxon(UUID synonymUUid,
3231
			UUID acceptedTaxonUuid) {
3232
		TaxonBase<?> base = this.load(synonymUUid);
3233
		Synonym syn = HibernateProxyHelper.deproxy(base, Synonym.class);
3234
		base = this.load(acceptedTaxonUuid);
3235
		Taxon taxon = HibernateProxyHelper.deproxy(base, Taxon.class);
3236

    
3237
		return this.swapSynonymAndAcceptedTaxon(syn, taxon);
3238
	}
3239

    
3240

    
3241
}
(99-99/105)