Project

General

Profile

Download (146 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.IZoologicalName;
100
import eu.etaxonomy.cdm.model.name.Rank;
101
import eu.etaxonomy.cdm.model.name.TaxonName;
102
import eu.etaxonomy.cdm.model.name.TaxonNameFactory;
103
import eu.etaxonomy.cdm.model.occurrence.DerivedUnit;
104
import eu.etaxonomy.cdm.model.occurrence.DeterminationEvent;
105
import eu.etaxonomy.cdm.model.occurrence.SpecimenOrObservationBase;
106
import eu.etaxonomy.cdm.model.reference.Reference;
107
import eu.etaxonomy.cdm.model.taxon.Classification;
108
import eu.etaxonomy.cdm.model.taxon.HomotypicGroupTaxonComparator;
109
import eu.etaxonomy.cdm.model.taxon.ITaxonTreeNode;
110
import eu.etaxonomy.cdm.model.taxon.Synonym;
111
import eu.etaxonomy.cdm.model.taxon.SynonymType;
112
import eu.etaxonomy.cdm.model.taxon.Taxon;
113
import eu.etaxonomy.cdm.model.taxon.TaxonBase;
114
import eu.etaxonomy.cdm.model.taxon.TaxonNode;
115
import eu.etaxonomy.cdm.model.taxon.TaxonRelationship;
116
import eu.etaxonomy.cdm.model.taxon.TaxonRelationshipType;
117
import eu.etaxonomy.cdm.persistence.dao.common.ICdmGenericDao;
118
import eu.etaxonomy.cdm.persistence.dao.common.IOrderedTermVocabularyDao;
119
import eu.etaxonomy.cdm.persistence.dao.initializer.AbstractBeanInitializer;
120
import eu.etaxonomy.cdm.persistence.dao.name.ITaxonNameDao;
121
import eu.etaxonomy.cdm.persistence.dao.occurrence.IOccurrenceDao;
122
import eu.etaxonomy.cdm.persistence.dao.taxon.IClassificationDao;
123
import eu.etaxonomy.cdm.persistence.dao.taxon.ITaxonDao;
124
import eu.etaxonomy.cdm.persistence.dao.taxon.ITaxonNodeDao;
125
import eu.etaxonomy.cdm.persistence.dto.UuidAndTitleCache;
126
import eu.etaxonomy.cdm.persistence.query.MatchMode;
127
import eu.etaxonomy.cdm.persistence.query.OrderHint;
128
import eu.etaxonomy.cdm.persistence.query.OrderHint.SortOrder;
129
import eu.etaxonomy.cdm.persistence.query.TaxonTitleType;
130
import eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy;
131

    
132

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

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

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

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

    
148
    @Autowired
149
    private ITaxonNodeDao taxonNodeDao;
150

    
151
    @Autowired
152
    private ITaxonNameDao nameDao;
153

    
154
    @Autowired
155
    private INameService nameService;
156

    
157
    @Autowired
158
    private IOccurrenceService occurrenceService;
159

    
160
    @Autowired
161
    private ITaxonNodeService nodeService;
162

    
163
    @Autowired
164
    private ICdmGenericDao genericDao;
165

    
166
    @Autowired
167
    private IDescriptionService descriptionService;
168

    
169
    @Autowired
170
    private IOrderedTermVocabularyDao orderedVocabularyDao;
171

    
172
    @Autowired
173
    private IOccurrenceDao occurrenceDao;
174

    
175
    @Autowired
176
    private IClassificationDao classificationDao;
177

    
178
    @Autowired
179
    private AbstractBeanInitializer beanInitializer;
180

    
181
    @Autowired
182
    private ILuceneIndexToolProvider luceneIndexToolProvider;
183

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

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

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

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

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

    
226

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

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

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

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

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

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

    
272
        return result;
273
    }
274

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

    
295

    
296

    
297

    
298
    @Override
299
    @Transactional(readOnly = false)
300
    public UpdateResult changeSynonymToRelatedTaxon(UUID synonymUuid,
301
            UUID toTaxonUuid,
302
            TaxonRelationshipType taxonRelationshipType,
303
            Reference citation,
304
            String microcitation){
305

    
306
        UpdateResult result = new UpdateResult();
307
        Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
308
        Synonym synonym = (Synonym) dao.load(synonymUuid);
309
        result = changeSynonymToRelatedTaxon(synonym, toTaxon, taxonRelationshipType, citation, microcitation);
310
        Taxon relatedTaxon = (Taxon)result.getCdmEntity();
311
//        result.setCdmEntity(relatedTaxon);
312
        result.addUpdatedObject(relatedTaxon);
313
        result.addUpdatedObject(toTaxon);
314
        return result;
315
    }
316

    
317
    @Override
318
    @Transactional(readOnly = false)
319
    public UpdateResult changeSynonymToRelatedTaxon(Synonym synonym, Taxon toTaxon, TaxonRelationshipType taxonRelationshipType, Reference citation, String microcitation){
320
        // Get name from synonym
321
        if (synonym == null){
322
            return null;
323
        }
324

    
325
        UpdateResult result = new UpdateResult();
326

    
327
        TaxonName synonymName = synonym.getName();
328

    
329
      /*  // remove synonym from taxon
330
        toTaxon.removeSynonym(synonym);
331
*/
332
        // Create a taxon with synonym name
333
        Taxon fromTaxon = Taxon.NewInstance(synonymName, null);
334
        save(fromTaxon);
335
        fromTaxon.setAppendedPhrase(synonym.getAppendedPhrase());
336

    
337
        // Add taxon relation
338
        fromTaxon.addTaxonRelation(toTaxon, taxonRelationshipType, citation, microcitation);
339
        result.setCdmEntity(fromTaxon);
340
        // since we are swapping names, we have to detach the name from the synonym completely.
341
        // Otherwise the synonym will still be in the list of typified names.
342
       // synonym.getName().removeTaxonBase(synonym);
343
        result.includeResult(this.deleteSynonym(synonym, null));
344

    
345
        return result;
346
    }
347

    
348
    @Transactional(readOnly = false)
349
    @Override
350
    public void changeHomotypicalGroupOfSynonym(Synonym synonym, HomotypicalGroup newHomotypicalGroup,
351
            Taxon targetTaxon, boolean setBasionymRelationIfApplicable){
352
        // Get synonym name
353
        TaxonName synonymName = synonym.getName();
354
        HomotypicalGroup oldHomotypicalGroup = synonymName.getHomotypicalGroup();
355

    
356
        // Switch groups
357
        oldHomotypicalGroup.removeTypifiedName(synonymName, false);
358
        newHomotypicalGroup.addTypifiedName(synonymName);
359

    
360
        //remove existing basionym relationships
361
        synonymName.removeBasionyms();
362

    
363
        //add basionym relationship
364
        if (setBasionymRelationIfApplicable){
365
            Set<TaxonName> basionyms = newHomotypicalGroup.getBasionyms();
366
            for (TaxonName basionym : basionyms){
367
                synonymName.addBasionym(basionym);
368
            }
369
        }
370

    
371
        //set synonym relationship correctly
372
        Taxon acceptedTaxon = synonym.getAcceptedTaxon();
373

    
374
        boolean hasNewTargetTaxon = targetTaxon != null && !targetTaxon.equals(acceptedTaxon);
375
        if (acceptedTaxon != null){
376

    
377
            HomotypicalGroup acceptedGroup = acceptedTaxon.getHomotypicGroup();
378
            boolean isHomotypicToTaxon = acceptedGroup.equals(newHomotypicalGroup);
379
            SynonymType newRelationType = isHomotypicToTaxon? SynonymType.HOMOTYPIC_SYNONYM_OF() : SynonymType.HETEROTYPIC_SYNONYM_OF();
380
            synonym.setType(newRelationType);
381

    
382
            if (hasNewTargetTaxon){
383
                acceptedTaxon.removeSynonym(synonym, false);
384
            }
385
        }
386
        if (hasNewTargetTaxon ){
387
            @SuppressWarnings("null")
388
            HomotypicalGroup acceptedGroup = targetTaxon.getHomotypicGroup();
389
            boolean isHomotypicToTaxon = acceptedGroup.equals(newHomotypicalGroup);
390
            SynonymType relType = isHomotypicToTaxon? SynonymType.HOMOTYPIC_SYNONYM_OF() : SynonymType.HETEROTYPIC_SYNONYM_OF();
391
            targetTaxon.addSynonym(synonym, relType);
392
        }
393

    
394
    }
395

    
396
    @Override
397
    @Transactional(readOnly = false)
398
    public void updateTitleCache(Class<? extends TaxonBase> clazz, Integer stepSize, IIdentifiableEntityCacheStrategy<TaxonBase> cacheStrategy, IProgressMonitor monitor) {
399
        if (clazz == null){
400
            clazz = TaxonBase.class;
401
        }
402
        super.updateTitleCacheImpl(clazz, stepSize, cacheStrategy, monitor);
403
    }
404

    
405
    @Override
406
    @Autowired
407
    protected void setDao(ITaxonDao dao) {
408
        this.dao = dao;
409
    }
410

    
411
    @Override
412
    public Pager<TaxonBase> findTaxaByName(Class<? extends TaxonBase> clazz, String uninomial,	String infragenericEpithet, String specificEpithet,	String infraspecificEpithet, String authorship, Rank rank, Integer pageSize,Integer pageNumber) {
413
        if (clazz == null){
414
            clazz = TaxonBase.class;
415
        }
416
        Integer numberOfResults = dao.countTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, rank);
417

    
418
        List<TaxonBase> results = new ArrayList<>();
419
        if(numberOfResults > 0) { // no point checking again
420
            results = dao.findTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, authorship, rank, pageSize, pageNumber);
421
        }
422

    
423
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
424
    }
425

    
426
    @Override
427
    public List<TaxonBase> listTaxaByName(Class<? extends TaxonBase> clazz, String uninomial, String infragenericEpithet, String specificEpithet,	String infraspecificEpithet, String authorship, Rank rank, Integer pageSize,Integer pageNumber) {
428
        if (clazz == null){
429
            clazz = TaxonBase.class;
430
        }
431
        Integer numberOfResults = dao.countTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, rank);
432

    
433
        List<TaxonBase> results = new ArrayList<>();
434
        if(numberOfResults > 0) { // no point checking again
435
            results = dao.findTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, authorship, rank, pageSize, pageNumber);
436
        }
437

    
438
        return results;
439
    }
440

    
441
    @Override
442
    public List<TaxonRelationship> listToTaxonRelationships(Taxon taxon, TaxonRelationshipType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths){
443
        Integer numberOfResults = dao.countTaxonRelationships(taxon, type, TaxonRelationship.Direction.relatedTo);
444

    
445
        List<TaxonRelationship> results = new ArrayList<>();
446
        if(numberOfResults > 0) { // no point checking again
447
            results = dao.getTaxonRelationships(taxon, type, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedTo);
448
        }
449
        return results;
450
    }
451

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

    
456
        List<TaxonRelationship> results = new ArrayList<>();
457
        if(numberOfResults > 0) { // no point checking again
458
            results = dao.getTaxonRelationships(taxon, type, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedTo);
459
        }
460
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
461
    }
462

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

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

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

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

    
485
    @Override
486
    public List<TaxonRelationship> listTaxonRelationships(Set<TaxonRelationshipType> types,
487
            Integer pageSize, Integer pageStart, List<OrderHint> orderHints, List<String> propertyPaths) {
488
        Long numberOfResults = dao.countTaxonRelationships(types);
489

    
490
        List<TaxonRelationship> results = new ArrayList<>();
491
        if(numberOfResults > 0) {
492
            results = dao.getTaxonRelationships(types, pageSize, pageStart, orderHints, propertyPaths);
493
        }
494
        return results;
495
    }
496

    
497
    @Override
498
    public Taxon findAcceptedTaxonFor(UUID synonymUuid, UUID classificationUuid, List<String> propertyPaths){
499

    
500
        Taxon result = null;
501
        Long count = 0l;
502

    
503
        Synonym synonym = null;
504

    
505
        try {
506
            synonym = (Synonym) dao.load(synonymUuid);
507
        } catch (ClassCastException e){
508
            throw new EntityNotFoundException("The TaxonBase entity referenced by " + synonymUuid + " is not a Synonmy");
509
        } catch (NullPointerException e){
510
            throw new EntityNotFoundException("No TaxonBase entity found for " + synonymUuid);
511
        }
512

    
513
        Classification classificationFilter = null;
514
        if(classificationUuid != null){
515
            try {
516
            classificationFilter = classificationDao.load(classificationUuid);
517
            } catch (NullPointerException e){
518
                throw new EntityNotFoundException("No Classification entity found for " + classificationUuid);
519
            }
520
            if(classificationFilter == null){
521

    
522
            }
523
        }
524

    
525
        count = dao.countAcceptedTaxonFor(synonym, classificationFilter) ;
526
        if(count > 0){
527
            result = dao.acceptedTaxonFor(synonym, classificationFilter, propertyPaths);
528
        }
529

    
530
        return result;
531
    }
532

    
533

    
534
    @Override
535
    public Set<Taxon> listRelatedTaxa(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, Integer maxDepth,
536
            Integer limit, Integer start, List<String> propertyPaths) {
537

    
538
        Set<Taxon> relatedTaxa = collectRelatedTaxa(taxon, includeRelationships, new HashSet<>(), maxDepth);
539
        relatedTaxa.remove(taxon);
540
        beanInitializer.initializeAll(relatedTaxa, propertyPaths);
541
        return relatedTaxa;
542
    }
543

    
544

    
545
    /**
546
     * recursively collect related taxa for the given <code>taxon</code> . The returned list will also include the
547
     *  <code>taxon</code> supplied as parameter.
548
     *
549
     * @param taxon
550
     * @param includeRelationships
551
     * @param taxa
552
     * @param maxDepth can be <code>null</code> for infinite depth
553
     * @return
554
     */
555
    private Set<Taxon> collectRelatedTaxa(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, Set<Taxon> taxa, Integer maxDepth) {
556

    
557
        if(taxa.isEmpty()) {
558
            taxa.add(taxon);
559
        }
560

    
561
        if(includeRelationships.isEmpty()){
562
            return taxa;
563
        }
564

    
565
        if(maxDepth != null) {
566
            maxDepth--;
567
        }
568
        if(logger.isDebugEnabled()){
569
            logger.debug("collecting related taxa for " + taxon + " with maxDepth=" + maxDepth);
570
        }
571
        List<TaxonRelationship> taxonRelationships = dao.getTaxonRelationships(taxon, null, null, null, null, null, null);
572
        for (TaxonRelationship taxRel : taxonRelationships) {
573

    
574
            // skip invalid data
575
            if (taxRel.getToTaxon() == null || taxRel.getFromTaxon() == null || taxRel.getType() == null) {
576
                continue;
577
            }
578
            // filter by includeRelationships
579
            for (TaxonRelationshipEdge relationshipEdgeFilter : includeRelationships) {
580
                if ( relationshipEdgeFilter.getTaxonRelationshipTypes().equals(taxRel.getType()) ) {
581
                    if (relationshipEdgeFilter.getDirections().contains(Direction.relatedTo) && !taxa.contains(taxRel.getToTaxon())) {
582
                        if(logger.isDebugEnabled()){
583
                            logger.debug(maxDepth + ": " + taxon.getTitleCache() + " --[" + taxRel.getType().getLabel() + "]--> " + taxRel.getToTaxon().getTitleCache());
584
                        }
585
                        taxa.add(taxRel.getToTaxon());
586
                        if(maxDepth == null || maxDepth > 0) {
587
                            taxa.addAll(collectRelatedTaxa(taxRel.getToTaxon(), includeRelationships, taxa, maxDepth));
588
                        }
589
                    }
590
                    if(relationshipEdgeFilter.getDirections().contains(Direction.relatedFrom) && !taxa.contains(taxRel.getFromTaxon())) {
591
                        taxa.add(taxRel.getFromTaxon());
592
                        if(logger.isDebugEnabled()){
593
                            logger.debug(maxDepth + ": " +taxRel.getFromTaxon().getTitleCache() + " --[" + taxRel.getType().getLabel() + "]--> " + taxon.getTitleCache() );
594
                        }
595
                        if(maxDepth == null || maxDepth > 0) {
596
                            taxa.addAll(collectRelatedTaxa(taxRel.getFromTaxon(), includeRelationships, taxa, maxDepth));
597
                        }
598
                    }
599
                }
600
            }
601
        }
602
        return taxa;
603
    }
604

    
605
    @Override
606
    public Pager<Synonym> getSynonyms(Taxon taxon,	SynonymType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
607
        Long numberOfResults = dao.countSynonyms(taxon, type);
608

    
609
        List<Synonym> results = new ArrayList<>();
610
        if(numberOfResults > 0) { // no point checking again
611
            results = dao.getSynonyms(taxon, type, pageSize, pageNumber, orderHints, propertyPaths);
612
        }
613

    
614
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
615
    }
616

    
617
    @Override
618
    public List<List<Synonym>> getSynonymsByHomotypicGroup(Taxon taxon, List<String> propertyPaths){
619
        List<List<Synonym>> result = new ArrayList<>();
620
        taxon = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
621
        HomotypicGroupTaxonComparator comparator = new HomotypicGroupTaxonComparator(taxon);
622

    
623

    
624
        //homotypic
625
        result.add(taxon.getHomotypicSynonymsByHomotypicGroup(comparator));
626

    
627
        //heterotypic
628
        List<HomotypicalGroup> homotypicalGroups = taxon.getHeterotypicSynonymyGroups();  //currently the list is sorted by the Taxon.defaultTaxonComparator
629
        for(HomotypicalGroup homotypicalGroup : homotypicalGroups){
630
            result.add(taxon.getSynonymsInGroup(homotypicalGroup, comparator));
631
        }
632

    
633
        return result;
634

    
635
    }
636

    
637
    @Override
638
    public List<Synonym> getHomotypicSynonymsByHomotypicGroup(Taxon taxon, List<String> propertyPaths){
639
        Taxon t = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
640
        HomotypicGroupTaxonComparator comparator = new HomotypicGroupTaxonComparator(taxon);
641

    
642
        return t.getHomotypicSynonymsByHomotypicGroup(comparator);
643
    }
644

    
645
    @Override
646
    public List<List<Synonym>> getHeterotypicSynonymyGroups(Taxon taxon, List<String> propertyPaths){
647
        Taxon t = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
648
        List<HomotypicalGroup> homotypicalGroups = t.getHeterotypicSynonymyGroups();
649
        List<List<Synonym>> heterotypicSynonymyGroups = new ArrayList<>(homotypicalGroups.size());
650
        for(HomotypicalGroup homotypicalGroup : homotypicalGroups){
651
            heterotypicSynonymyGroups.add(t.getSynonymsInGroup(homotypicalGroup));
652
        }
653
        return heterotypicSynonymyGroups;
654
    }
655

    
656
    @Override
657
    public List<UuidAndTitleCache<IdentifiableEntity>> findTaxaAndNamesForEditor(IFindTaxaAndNamesConfigurator configurator){
658

    
659
        List<UuidAndTitleCache<IdentifiableEntity>> results = new ArrayList<>();
660

    
661

    
662
        if (configurator.isDoSynonyms() || configurator.isDoTaxa() || configurator.isDoNamesWithoutTaxa() || configurator.isDoTaxaByCommonNames()){
663
        	results = dao.getTaxaByNameForEditor(configurator.isDoTaxa(), configurator.isDoSynonyms(), configurator.isDoNamesWithoutTaxa(), configurator.isDoMisappliedNames(), configurator.isDoTaxaByCommonNames(), configurator.getTitleSearchStringSqlized(), configurator.getClassification(), configurator.getMatchMode(), configurator.getNamedAreas(), configurator.getOrder());
664
        }
665

    
666
        return results;
667
    }
668

    
669
    @Override
670
    public Pager<IdentifiableEntity> findTaxaAndNames(IFindTaxaAndNamesConfigurator configurator) {
671

    
672
        List<IdentifiableEntity> results = new ArrayList<>();
673
        long numberOfResults = 0; // overall number of results (as opposed to number of results per page)
674
        List<TaxonBase> taxa = null;
675

    
676
        // Taxa and synonyms
677
        long numberTaxaResults = 0L;
678

    
679

    
680
        List<String> propertyPath = new ArrayList<>();
681
        if(configurator.getTaxonPropertyPath() != null){
682
            propertyPath.addAll(configurator.getTaxonPropertyPath());
683
        }
684

    
685

    
686
       if (configurator.isDoMisappliedNames() || configurator.isDoSynonyms() || configurator.isDoTaxa() || configurator.isDoTaxaByCommonNames()){
687
            if(configurator.getPageSize() != null){ // no point counting if we need all anyway
688
                numberTaxaResults =
689
                    dao.countTaxaByName(configurator.isDoTaxa(),configurator.isDoSynonyms(), configurator.isDoMisappliedNames(),
690
                        configurator.isDoTaxaByCommonNames(), configurator.isDoIncludeAuthors(), configurator.getTitleSearchStringSqlized(),
691
                        configurator.getClassification(), configurator.getMatchMode(),
692
                        configurator.getNamedAreas());
693
            }
694

    
695
            if(configurator.getPageSize() == null || numberTaxaResults > configurator.getPageSize() * configurator.getPageNumber()){ // no point checking again if less results
696
                taxa = dao.getTaxaByName(configurator.isDoTaxa(), configurator.isDoSynonyms(),
697
                    configurator.isDoMisappliedNames(), configurator.isDoTaxaByCommonNames(), configurator.isDoIncludeAuthors(),
698
                    configurator.getTitleSearchStringSqlized(), configurator.getClassification(),
699
                    configurator.getMatchMode(), configurator.getNamedAreas(), configurator.getOrder(),
700
                    configurator.getPageSize(), configurator.getPageNumber(), propertyPath);
701
            }
702
       }
703

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

    
706
        if(taxa != null){
707
            results.addAll(taxa);
708
        }
709

    
710
        numberOfResults += numberTaxaResults;
711

    
712
        // Names without taxa
713
        if (configurator.isDoNamesWithoutTaxa()) {
714
            int numberNameResults = 0;
715

    
716
            List<? extends TaxonName> names =
717
                nameDao.findByName(configurator.isDoIncludeAuthors(), configurator.getTitleSearchStringSqlized(), configurator.getMatchMode(),
718
                        configurator.getPageSize(), configurator.getPageNumber(), null, configurator.getTaxonNamePropertyPath());
719
            if (logger.isDebugEnabled()) { logger.debug(names.size() + " matching name(s) found"); }
720
            if (names.size() > 0) {
721
                for (TaxonName taxonName : names) {
722
                    if (taxonName.getTaxonBases().size() == 0) {
723
                        results.add(taxonName);
724
                        numberNameResults++;
725
                    }
726
                }
727
                if (logger.isDebugEnabled()) { logger.debug(numberNameResults + " matching name(s) without taxa found"); }
728
                numberOfResults += numberNameResults;
729
            }
730
        }
731

    
732

    
733

    
734
       return new DefaultPagerImpl<>
735
            (configurator.getPageNumber(), numberOfResults, configurator.getPageSize(), results);
736
    }
737

    
738
    public List<UuidAndTitleCache<TaxonBase>> getTaxonUuidAndTitleCache(Integer limit, String pattern){
739
        return dao.getUuidAndTitleCache(limit, pattern);
740
    }
741

    
742
    @Override
743
    public List<MediaRepresentation> getAllMedia(Taxon taxon, int size, int height, int widthOrDuration, String[] mimeTypes){
744
        List<MediaRepresentation> medRep = new ArrayList<>();
745
        taxon = (Taxon)dao.load(taxon.getUuid());
746
        Set<TaxonDescription> descriptions = taxon.getDescriptions();
747
        for (TaxonDescription taxDesc: descriptions){
748
            Set<DescriptionElementBase> elements = taxDesc.getElements();
749
            for (DescriptionElementBase descElem: elements){
750
                for(Media media : descElem.getMedia()){
751

    
752
                    //find the best matching representation
753
                    medRep.add(MediaUtils.findBestMatchingRepresentation(media, null, size, height, widthOrDuration, mimeTypes));
754

    
755
                }
756
            }
757
        }
758
        return medRep;
759
    }
760

    
761
    @Override
762
    public List<Media> listTaxonDescriptionMedia(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, boolean limitToGalleries, List<String> propertyPath){
763
        return listMedia(taxon, includeRelationships, limitToGalleries, true, false, false, propertyPath);
764
    }
765

    
766
    @Override
767
    public List<Media> listMedia(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships,
768
            Boolean limitToGalleries, Boolean includeTaxonDescriptions, Boolean includeOccurrences,
769
            Boolean includeTaxonNameDescriptions, List<String> propertyPath) {
770

    
771
    //    logger.setLevel(Level.TRACE);
772
//        Logger.getLogger("org.hibernate.SQL").setLevel(Level.TRACE);
773

    
774
        logger.trace("listMedia() - START");
775

    
776
        Set<Taxon> taxa = new HashSet<>();
777
        List<Media> taxonMedia = new ArrayList<>();
778
        List<Media> nonImageGalleryImages = new ArrayList<>();
779

    
780
        if (limitToGalleries == null) {
781
            limitToGalleries = false;
782
        }
783

    
784
        // --- resolve related taxa
785
        if (includeRelationships != null && ! includeRelationships.isEmpty()) {
786
            logger.trace("listMedia() - resolve related taxa");
787
            taxa = listRelatedTaxa(taxon, includeRelationships, null, null, null, null);
788
        }
789

    
790
        taxa.add((Taxon) dao.load(taxon.getUuid()));
791

    
792
        if(includeTaxonDescriptions != null && includeTaxonDescriptions){
793
            logger.trace("listMedia() - includeTaxonDescriptions");
794
            List<TaxonDescription> taxonDescriptions = new ArrayList<>();
795
            // --- TaxonDescriptions
796
            for (Taxon t : taxa) {
797
                taxonDescriptions.addAll(descriptionService.listTaxonDescriptions(t, null, null, null, null, propertyPath));
798
            }
799
            for (TaxonDescription taxonDescription : taxonDescriptions) {
800
                if (!limitToGalleries || taxonDescription.isImageGallery()) {
801
                    for (DescriptionElementBase element : taxonDescription.getElements()) {
802
                        for (Media media : element.getMedia()) {
803
                            if(taxonDescription.isImageGallery()){
804
                                taxonMedia.add(media);
805
                            }
806
                            else{
807
                                nonImageGalleryImages.add(media);
808
                            }
809
                        }
810
                    }
811
                }
812
            }
813
            //put images from image gallery first (#3242)
814
            taxonMedia.addAll(nonImageGalleryImages);
815
        }
816

    
817

    
818
        if(includeOccurrences != null && includeOccurrences) {
819
            logger.trace("listMedia() - includeOccurrences");
820
            Set<SpecimenOrObservationBase> specimensOrObservations = new HashSet<>();
821
            // --- Specimens
822
            for (Taxon t : taxa) {
823
                specimensOrObservations.addAll(occurrenceDao.listByAssociatedTaxon(null, t, null, null, null, null));
824
            }
825
            for (SpecimenOrObservationBase occurrence : specimensOrObservations) {
826

    
827
//            	direct media removed from specimen #3597
828
//              taxonMedia.addAll(occurrence.getMedia());
829

    
830
                // SpecimenDescriptions
831
                Set<SpecimenDescription> specimenDescriptions = occurrence.getSpecimenDescriptions();
832
                for (DescriptionBase specimenDescription : specimenDescriptions) {
833
                    if (!limitToGalleries || specimenDescription.isImageGallery()) {
834
                        Set<DescriptionElementBase> elements = specimenDescription.getElements();
835
                        for (DescriptionElementBase element : elements) {
836
                            for (Media media : element.getMedia()) {
837
                                taxonMedia.add(media);
838
                            }
839
                        }
840
                    }
841
                }
842

    
843
                if (occurrence.isInstanceOf(DerivedUnit.class)) {
844
                    DerivedUnit derivedUnit = CdmBase.deproxy(occurrence, DerivedUnit.class);
845
                    // Collection
846
                    //TODO why may collections have media attached? #
847
                    if (derivedUnit.getCollection() != null){
848
                        taxonMedia.addAll(derivedUnit.getCollection().getMedia());
849
                    }
850
                }
851
                //media in hierarchy
852
                taxonMedia.addAll(occurrenceService.getMediainHierarchy(occurrence, null, null, propertyPath).getRecords());
853
            }
854
        }
855

    
856
        if(includeTaxonNameDescriptions != null && includeTaxonNameDescriptions) {
857
            logger.trace("listMedia() - includeTaxonNameDescriptions");
858
            // --- TaxonNameDescription
859
            Set<TaxonNameDescription> nameDescriptions = new HashSet<>();
860
            for (Taxon t : taxa) {
861
                nameDescriptions .addAll(t.getName().getDescriptions());
862
            }
863
            for(TaxonNameDescription nameDescription: nameDescriptions){
864
                if (!limitToGalleries || nameDescription.isImageGallery()) {
865
                    Set<DescriptionElementBase> elements = nameDescription.getElements();
866
                    for (DescriptionElementBase element : elements) {
867
                        for (Media media : element.getMedia()) {
868
                            taxonMedia.add(media);
869
                        }
870
                    }
871
                }
872
            }
873
        }
874

    
875

    
876
        logger.trace("listMedia() - initialize");
877
        beanInitializer.initializeAll(taxonMedia, propertyPath);
878

    
879
        logger.trace("listMedia() - END");
880

    
881
        return taxonMedia;
882
    }
883

    
884
    @Override
885
    public List<TaxonBase> findTaxaByID(Set<Integer> listOfIDs) {
886
        return this.dao.loadList(listOfIDs, null);
887
    }
888

    
889
    @Override
890
    public TaxonBase findTaxonByUuid(UUID uuid, List<String> propertyPaths){
891
        return this.dao.findByUuid(uuid, null ,propertyPaths);
892
    }
893

    
894
    @Override
895
    public int countSynonyms(boolean onlyAttachedToTaxon){
896
        return this.dao.countSynonyms(onlyAttachedToTaxon);
897
    }
898

    
899
    @Override
900
    public List<TaxonName> findIdenticalTaxonNames(List<String> propertyPath) {
901
        return this.dao.findIdenticalTaxonNames(propertyPath);
902
    }
903

    
904
    @Override
905
    @Transactional(readOnly=false)
906
    public DeleteResult deleteTaxon(UUID taxonUUID, TaxonDeletionConfigurator config, UUID classificationUuid)  {
907

    
908
    	if (config == null){
909
            config = new TaxonDeletionConfigurator();
910
        }
911
    	Taxon taxon = (Taxon)dao.load(taxonUUID);
912
    	DeleteResult result = new DeleteResult();
913
    	if (taxon == null){
914
    	    result.setAbort();
915
    	    result.addException(new Exception ("The taxon was already deleted."));
916
    	    return result;
917
    	}
918
    	taxon = HibernateProxyHelper.deproxy(taxon);
919
    	Classification classification = HibernateProxyHelper.deproxy(classificationDao.load(classificationUuid), Classification.class);
920
        result = isDeletable(taxonUUID, config);
921

    
922
        if (result.isOk()){
923
            // --- DeleteSynonymRelations
924
            if (config.isDeleteSynonymRelations()){
925
                boolean removeSynonymNameFromHomotypicalGroup = false;
926
                // use tmp Set to avoid concurrent modification
927
                Set<Synonym> synsToDelete = new HashSet<>();
928
                synsToDelete.addAll(taxon.getSynonyms());
929
                for (Synonym synonym : synsToDelete){
930
                    taxon.removeSynonym(synonym, removeSynonymNameFromHomotypicalGroup);
931

    
932
                    // --- DeleteSynonymsIfPossible
933
                    if (config.isDeleteSynonymsIfPossible()){
934
                        //TODO which value
935
                        boolean newHomotypicGroupIfNeeded = true;
936
                        SynonymDeletionConfigurator synConfig = new SynonymDeletionConfigurator();
937
                        result.includeResult(deleteSynonym(synonym, synConfig));
938
                    }
939
                }
940
            }
941

    
942
            // --- DeleteTaxonRelationships
943
            if (! config.isDeleteTaxonRelationships()){
944
                if (taxon.getTaxonRelations().size() > 0){
945
                    result.setAbort();
946
                    result.addException(new Exception("Taxon can't be deleted as it is related to another taxon. " +
947
                            "Remove taxon from all relations to other taxa prior to deletion."));
948

    
949
                }
950
            } else{
951
                TaxonDeletionConfigurator configRelTaxon = new TaxonDeletionConfigurator();
952
                configRelTaxon.setDeleteTaxonNodes(false);
953
                configRelTaxon.setDeleteConceptRelationships(true);
954

    
955
                for (TaxonRelationship taxRel: taxon.getTaxonRelations()){
956
                    if (config.isDeleteMisappliedNamesAndInvalidDesignations()
957
                            && taxRel.getType().isMisappliedNameOrInvalidDesignation()
958
                            && taxon.equals(taxRel.getToTaxon())){
959
                        this.deleteTaxon(taxRel.getFromTaxon().getUuid(), config, classificationUuid);
960
                    } else if (config.isDeleteConceptRelationships() && taxRel.getType().isConceptRelationship()){
961

    
962
                        if (taxon.equals(taxRel.getToTaxon()) && isDeletable(taxRel.getFromTaxon().getUuid(), configRelTaxon).isOk()){
963
                            this.deleteTaxon(taxRel.getFromTaxon().getUuid(), configRelTaxon, classificationUuid);
964
                        }else if (isDeletable(taxRel.getToTaxon().getUuid(), configRelTaxon).isOk()){
965
                            this.deleteTaxon(taxRel.getToTaxon().getUuid(), configRelTaxon, classificationUuid);
966
                        }
967
                    }
968
                    taxon.removeTaxonRelation(taxRel);
969

    
970
                }
971
            }
972

    
973
            //    	TaxonDescription
974
            if (config.isDeleteDescriptions()){
975
                Set<TaxonDescription> descriptions = taxon.getDescriptions();
976
                List<TaxonDescription> removeDescriptions = new ArrayList<>();
977
                for (TaxonDescription desc: descriptions){
978
                    //TODO use description delete configurator ?
979
                    //FIXME check if description is ALWAYS deletable
980
                    if (desc.getDescribedSpecimenOrObservation() != null){
981
                        result.setAbort();
982
                        result.addException(new Exception("Taxon can't be deleted as it is used in a TaxonDescription" +
983
                                " which also describes specimens or observations"));
984
                        break;
985
                    }
986
                    removeDescriptions.add(desc);
987

    
988

    
989
                }
990
                if (result.isOk()){
991
                    for (TaxonDescription desc: removeDescriptions){
992
                        taxon.removeDescription(desc);
993
                        descriptionService.delete(desc);
994
                    }
995
                } else {
996
                    return result;
997
                }
998
            }
999

    
1000

    
1001
         if (! config.isDeleteTaxonNodes() || (!config.isDeleteInAllClassifications() && classification == null && taxon.getTaxonNodes().size() > 1)){
1002
             result.addException(new Exception( "Taxon can't be deleted as it is used in more than one classification."));
1003
         }else{
1004
             if (taxon.getTaxonNodes().size() != 0){
1005
                Set<TaxonNode> nodes = taxon.getTaxonNodes();
1006
                Iterator<TaxonNode> iterator = nodes.iterator();
1007
                TaxonNode node = null;
1008
                boolean deleteChildren;
1009
                if (config.getTaxonNodeConfig().getChildHandling().equals(ChildHandling.DELETE)){
1010
                    deleteChildren = true;
1011
                }else {
1012
                    deleteChildren = false;
1013
                }
1014
                boolean success = true;
1015
                if (!config.isDeleteInAllClassifications() && !(classification == null)){
1016
                    while (iterator.hasNext()){
1017
                        node = iterator.next();
1018
                        if (node.getClassification().equals(classification)){
1019
                            break;
1020
                        }
1021
                        node = null;
1022
                    }
1023
                    if (node != null){
1024
                        HibernateProxyHelper.deproxy(node, TaxonNode.class);
1025
                        success =taxon.removeTaxonNode(node, deleteChildren);
1026
                        nodeService.delete(node);
1027
                        result.addDeletedObject(node);
1028
                    } else {
1029
                    	result.setError();
1030
                    	result.addException(new Exception("The taxon can not be deleted because it is not used in defined classification."));
1031
                    }
1032
                } else if (config.isDeleteInAllClassifications()){
1033
                    List<TaxonNode> nodesList = new ArrayList<>();
1034
                    nodesList.addAll(taxon.getTaxonNodes());
1035
                    for (ITaxonTreeNode treeNode: nodesList){
1036
                        TaxonNode taxonNode = (TaxonNode) treeNode;
1037
                        if(!deleteChildren){
1038
                            Object[] childNodes = taxonNode.getChildNodes().toArray();
1039
                            for (Object childNode: childNodes){
1040
                                TaxonNode childNodeCast = (TaxonNode) childNode;
1041
                                taxonNode.getParent().addChildNode(childNodeCast, childNodeCast.getReference(), childNodeCast.getMicroReference());
1042
                            }
1043
                        }
1044
                    }
1045
                    config.getTaxonNodeConfig().setDeleteElement(false);
1046
                    DeleteResult resultNodes = nodeService.deleteTaxonNodes(nodesList, config);
1047
                    if (!resultNodes.isOk()){
1048
                    	result.addExceptions(resultNodes.getExceptions());
1049
                    	result.setStatus(resultNodes.getStatus());
1050
                    } else {
1051
                        result.addUpdatedObjects(resultNodes.getUpdatedObjects());
1052
                    }
1053
                }
1054
                if (!success){
1055
                    result.setError();
1056
                    result.addException(new Exception("The taxon can not be deleted because the taxon node can not be removed."));
1057
                }
1058
            }
1059
         }
1060
         TaxonName name = taxon.getName();
1061
         taxon.setName(null);
1062
         this.saveOrUpdate(taxon);
1063

    
1064
         if ((taxon.getTaxonNodes() == null || taxon.getTaxonNodes().size()== 0)  && result.isOk()){
1065
             try{
1066
                 UUID uuid = dao.delete(taxon);
1067
                 result.addDeletedObject(taxon);
1068
             }catch(Exception e){
1069
                 result.addException(e);
1070
                 result.setError();
1071
             }
1072
         } else {
1073
             result.setError();
1074
             result.addException(new Exception("The Taxon can't be deleted because it is used in a classification."));
1075

    
1076
         }
1077
            //TaxonName
1078
        if (config.isDeleteNameIfPossible() && result.isOk()){
1079
            DeleteResult nameResult = new DeleteResult();
1080
            //remove name if possible (and required)
1081
            if (name != null ){
1082
                nameResult = nameService.delete(name.getUuid(), config.getNameDeletionConfig());
1083
            }
1084
            if (nameResult.isError() || nameResult.isAbort()){
1085
                result.addRelatedObject(name);
1086
                result.addExceptions(nameResult.getExceptions());
1087
            }else{
1088
                result.includeResult(nameResult);
1089
            }
1090

    
1091

    
1092
       }
1093
       }
1094

    
1095
        return result;
1096

    
1097
    }
1098

    
1099
    private String checkForReferences(Taxon taxon){
1100
        Set<CdmBase> referencingObjects = genericDao.getReferencingObjects(taxon);
1101
        for (CdmBase referencingObject : referencingObjects){
1102
            //IIdentificationKeys (Media, Polytomous, MultiAccess)
1103
            if (HibernateProxyHelper.isInstanceOf(referencingObject, IIdentificationKey.class)){
1104
                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";
1105

    
1106
                return message;
1107
            }
1108

    
1109

    
1110
           /* //PolytomousKeyNode
1111
            if (referencingObject.isInstanceOf(PolytomousKeyNode.class)){
1112
                String message = "Taxon" + taxon.getTitleCache() + " can't be deleted as it is used in polytomous key node";
1113
                return message;
1114
            }*/
1115

    
1116
            //TaxonInteraction
1117
            if (referencingObject.isInstanceOf(TaxonInteraction.class)){
1118
                String message = "Taxon can't be deleted as it is used in taxonInteraction#taxon2";
1119
                return message;
1120
            }
1121

    
1122
          //TaxonInteraction
1123
            if (referencingObject.isInstanceOf(DeterminationEvent.class)){
1124
                String message = "Taxon can't be deleted as it is used in a determination event";
1125
                return message;
1126
            }
1127

    
1128
        }
1129

    
1130
        referencingObjects = null;
1131
        return null;
1132
    }
1133

    
1134
    private boolean checkForPolytomousKeys(Taxon taxon){
1135
        boolean result = false;
1136
        List<CdmBase> list = genericDao.getCdmBasesByFieldAndClass(PolytomousKeyNode.class, "taxon", taxon, null);
1137
        if (!list.isEmpty()) {
1138
            result = true;
1139
        }
1140
        return result;
1141
    }
1142

    
1143
    @Override
1144
    @Transactional(readOnly = false)
1145
    public DeleteResult delete(UUID synUUID){
1146
    	DeleteResult result = new DeleteResult();
1147
    	Synonym syn = (Synonym)dao.load(synUUID);
1148

    
1149
        return this.deleteSynonym(syn, null);
1150
    }
1151

    
1152
    @Override
1153
    @Transactional(readOnly = false)
1154
    public DeleteResult deleteSynonym(UUID synonymUuid, SynonymDeletionConfigurator config) {
1155
        return deleteSynonym((Synonym)dao.load(synonymUuid), config);
1156

    
1157
    }
1158

    
1159

    
1160
    @Override
1161
    @Transactional(readOnly = false)
1162
    public DeleteResult deleteSynonym(Synonym synonym, SynonymDeletionConfigurator config) {
1163
        DeleteResult result = new DeleteResult();
1164
    	if (synonym == null){
1165
    		result.setAbort();
1166
    		result.addException(new Exception("The synonym was already deleted."));
1167
    		return result;
1168
        }
1169

    
1170
        if (config == null){
1171
            config = new SynonymDeletionConfigurator();
1172
        }
1173

    
1174
        result = isDeletable(synonym.getUuid(), config);
1175

    
1176

    
1177
        if (result.isOk()){
1178

    
1179
            synonym = HibernateProxyHelper.deproxy(this.load(synonym.getUuid()), Synonym.class);
1180

    
1181
            //remove synonym
1182
            Taxon accTaxon = synonym.getAcceptedTaxon();
1183

    
1184
            if (accTaxon != null){
1185
                accTaxon = HibernateProxyHelper.deproxy(accTaxon, Taxon.class);
1186
                accTaxon.removeSynonym(synonym, false);
1187
                this.saveOrUpdate(accTaxon);
1188
                result.addUpdatedObject(accTaxon);
1189
            }
1190
            this.saveOrUpdate(synonym);
1191
            //#6281
1192
            dao.flush();
1193

    
1194
            TaxonName name = synonym.getName();
1195
            synonym.setName(null);
1196

    
1197
            dao.delete(synonym);
1198
            result.addDeletedObject(synonym);
1199

    
1200
            //remove name if possible (and required)
1201
            if (name != null && config.isDeleteNameIfPossible()){
1202

    
1203
                    DeleteResult nameDeleteResult = nameService.delete(name, config.getNameDeletionConfig());
1204
                    if (nameDeleteResult.isAbort() || nameDeleteResult.isError()){
1205
                    	result.addExceptions(nameDeleteResult.getExceptions());
1206
                    	result.addRelatedObject(name);
1207
                    }else{
1208
                        result.addDeletedObject(name);
1209
                    }
1210
            }
1211

    
1212
        }
1213
        return result;
1214
    }
1215

    
1216
    @Override
1217
    public List<TaxonName> findIdenticalTaxonNameIds(List<String> propertyPath) {
1218

    
1219
        return this.dao.findIdenticalNamesNew(propertyPath);
1220
    }
1221
//
1222
//    @Override
1223
//    public String getPhylumName(TaxonName name){
1224
//        return this.dao.getPhylumName(name);
1225
//    }
1226

    
1227
    @Override
1228
    public Taxon findBestMatchingTaxon(String taxonName) {
1229
        MatchingTaxonConfigurator config = MatchingTaxonConfigurator.NewInstance();
1230
        config.setTaxonNameTitle(taxonName);
1231
        return findBestMatchingTaxon(config);
1232
    }
1233

    
1234
    @Override
1235
    public Taxon findBestMatchingTaxon(MatchingTaxonConfigurator config) {
1236

    
1237
        Taxon bestCandidate = null;
1238
        try{
1239
            // 1. search for accepted taxa
1240
            List<TaxonBase> taxonList = dao.findByNameTitleCache(true, false, config.getTaxonNameTitle(), null, MatchMode.EXACT, null, null, 0, null, null);
1241
            boolean bestCandidateMatchesSecUuid = false;
1242
            boolean bestCandidateIsInClassification = false;
1243
            int countEqualCandidates = 0;
1244
            for(TaxonBase taxonBaseCandidate : taxonList){
1245
                if(taxonBaseCandidate instanceof Taxon){
1246
                    Taxon newCanditate = CdmBase.deproxy(taxonBaseCandidate, Taxon.class);
1247
                    boolean newCandidateMatchesSecUuid = isMatchesSecUuid(newCanditate, config);
1248
                    if (! newCandidateMatchesSecUuid && config.isOnlyMatchingSecUuid() ){
1249
                        continue;
1250
                    }else if(newCandidateMatchesSecUuid && ! bestCandidateMatchesSecUuid){
1251
                        bestCandidate = newCanditate;
1252
                        countEqualCandidates = 1;
1253
                        bestCandidateMatchesSecUuid = true;
1254
                        continue;
1255
                    }
1256

    
1257
                    boolean newCandidateInClassification = isInClassification(newCanditate, config);
1258
                    if (! newCandidateInClassification && config.isOnlyMatchingClassificationUuid()){
1259
                        continue;
1260
                    }else if (newCandidateInClassification && ! bestCandidateIsInClassification){
1261
                        bestCandidate = newCanditate;
1262
                        countEqualCandidates = 1;
1263
                        bestCandidateIsInClassification = true;
1264
                        continue;
1265
                    }
1266
                    if (bestCandidate == null){
1267
                        bestCandidate = newCanditate;
1268
                        countEqualCandidates = 1;
1269
                        continue;
1270
                    }
1271

    
1272
                }else{  //not Taxon.class
1273
                    continue;
1274
                }
1275
                countEqualCandidates++;
1276

    
1277
            }
1278
            if (bestCandidate != null){
1279
                if(countEqualCandidates > 1){
1280
                    logger.info(countEqualCandidates + " equally matching TaxonBases found, using first accepted Taxon: " + bestCandidate.getTitleCache());
1281
                    return bestCandidate;
1282
                } else {
1283
                    logger.info("using accepted Taxon: " + bestCandidate.getTitleCache());
1284
                    return bestCandidate;
1285
                }
1286
            }
1287

    
1288

    
1289
            // 2. search for synonyms
1290
            if (config.isIncludeSynonyms()){
1291
                List<TaxonBase> synonymList = dao.findByNameTitleCache(false, true, config.getTaxonNameTitle(), null, MatchMode.EXACT, null, null, 0, null, null);
1292
                for(TaxonBase taxonBase : synonymList){
1293
                    if(taxonBase instanceof Synonym){
1294
                        Synonym synonym = CdmBase.deproxy(taxonBase, Synonym.class);
1295
                        bestCandidate = synonym.getAcceptedTaxon();
1296
                        if(bestCandidate != null){
1297
                            logger.info("using accepted Taxon " +  bestCandidate.getTitleCache() + " for synonym " + taxonBase.getTitleCache());
1298
                            return bestCandidate;
1299
                        }
1300
                        //TODO extend method: search using treeUUID, using SecUUID, first find accepted then include synonyms until a matching taxon is found
1301
                    }
1302
                }
1303
            }
1304

    
1305
        } catch (Exception e){
1306
            logger.error(e);
1307
            e.printStackTrace();
1308
        }
1309

    
1310
        return bestCandidate;
1311
    }
1312

    
1313
    private boolean isInClassification(Taxon taxon, MatchingTaxonConfigurator config) {
1314
        UUID configClassificationUuid = config.getClassificationUuid();
1315
        if (configClassificationUuid == null){
1316
            return false;
1317
        }
1318
        for (TaxonNode node : taxon.getTaxonNodes()){
1319
            UUID classUuid = node.getClassification().getUuid();
1320
            if (configClassificationUuid.equals(classUuid)){
1321
                return true;
1322
            }
1323
        }
1324
        return false;
1325
    }
1326

    
1327
    private boolean isMatchesSecUuid(Taxon taxon, MatchingTaxonConfigurator config) {
1328
        UUID configSecUuid = config.getSecUuid();
1329
        if (configSecUuid == null){
1330
            return false;
1331
        }
1332
        UUID taxonSecUuid = (taxon.getSec() == null)? null : taxon.getSec().getUuid();
1333
        return configSecUuid.equals(taxonSecUuid);
1334
    }
1335

    
1336
    @Override
1337
    public Synonym findBestMatchingSynonym(String taxonName) {
1338
        List<TaxonBase> synonymList = dao.findByNameTitleCache(false, true, taxonName, null, MatchMode.EXACT, null, null, 0, null, null);
1339
        if(! synonymList.isEmpty()){
1340
            Synonym result = CdmBase.deproxy(synonymList.iterator().next(), Synonym.class);
1341
            if(synonymList.size() == 1){
1342
                logger.info(synonymList.size() + " Synonym found " + result.getTitleCache() );
1343
                return result;
1344
            } else {
1345
                logger.info("Several matching synonyms found. Using first: " +  result.getTitleCache());
1346
                return result;
1347
            }
1348
        }
1349
        return null;
1350
    }
1351

    
1352
    @Override
1353
    @Transactional(readOnly = false)
1354
    public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym,
1355
            Taxon newTaxon,
1356
            boolean moveHomotypicGroup,
1357
            SynonymType newSynonymType) throws HomotypicalGroupChangeException {
1358
        return moveSynonymToAnotherTaxon(oldSynonym, newTaxon, moveHomotypicGroup,
1359
                newSynonymType,
1360
                oldSynonym.getSec(),
1361
                oldSynonym.getSecMicroReference(),
1362
                true);
1363
    }
1364

    
1365
    @Override
1366
    @Transactional(readOnly = false)
1367
    public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym,
1368
            Taxon newTaxon,
1369
            boolean moveHomotypicGroup,
1370
            SynonymType newSynonymType,
1371
            Reference newSecundum,
1372
            String newSecundumDetail,
1373
            boolean keepSecundumIfUndefined) throws HomotypicalGroupChangeException {
1374

    
1375
        Synonym synonym = CdmBase.deproxy(dao.load(oldSynonym.getUuid()), Synonym.class);
1376
        Taxon oldTaxon = CdmBase.deproxy(dao.load(synonym.getAcceptedTaxon().getUuid()), Taxon.class);
1377
        //TODO what if there is no name ?? Concepts may be cached (e.g. via TCS import)
1378
        TaxonName synonymName = synonym.getName();
1379
        TaxonName fromTaxonName = oldTaxon.getName();
1380
        //set default relationship type
1381
        if (newSynonymType == null){
1382
            newSynonymType = SynonymType.HETEROTYPIC_SYNONYM_OF();
1383
        }
1384
        boolean newRelTypeIsHomotypic = newSynonymType.equals(SynonymType.HOMOTYPIC_SYNONYM_OF());
1385

    
1386
        HomotypicalGroup homotypicGroup = synonymName.getHomotypicalGroup();
1387
        int hgSize = homotypicGroup.getTypifiedNames().size();
1388
        boolean isSingleInGroup = !(hgSize > 1);
1389

    
1390
        if (! isSingleInGroup){
1391
            boolean isHomotypicToAccepted = synonymName.isHomotypic(fromTaxonName);
1392
            boolean hasHomotypicSynonymRelatives = isHomotypicToAccepted ? hgSize > 2 : hgSize > 1;
1393
            if (isHomotypicToAccepted){
1394
                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.";
1395
                String homotypicRelatives = hasHomotypicSynonymRelatives ? " and other synonym(s)":"";
1396
                message = String.format(message, homotypicRelatives);
1397
                throw new HomotypicalGroupChangeException(message);
1398
            }
1399
            if (! moveHomotypicGroup){
1400
                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.";
1401
                throw new HomotypicalGroupChangeException(message);
1402
            }
1403
        }else{
1404
            moveHomotypicGroup = true;  //single synonym always allows to moveCompleteGroup
1405
        }
1406
//        Assert.assertTrue("Synonym can only be moved with complete homotypic group", moveHomotypicGroup);
1407

    
1408
        UpdateResult result = new UpdateResult();
1409
        //move all synonyms to new taxon
1410
        List<Synonym> homotypicSynonyms = oldTaxon.getSynonymsInGroup(homotypicGroup);
1411
        for (Synonym synRelation: homotypicSynonyms){
1412

    
1413
            newTaxon = HibernateProxyHelper.deproxy(newTaxon, Taxon.class);
1414
            oldTaxon = HibernateProxyHelper.deproxy(oldTaxon, Taxon.class);
1415
            newTaxon.addSynonym(synRelation, newSynonymType);
1416
            oldTaxon.removeSynonym(synRelation, false);
1417
            if (newSecundum != null || !keepSecundumIfUndefined){
1418
                synRelation.setSec(newSecundum);
1419
            }
1420
            if (newSecundumDetail != null || !keepSecundumIfUndefined){
1421
                synRelation.setSecMicroReference(newSecundumDetail);
1422
            }
1423

    
1424
            //set result  //why is this needed? Seems wrong to me (AM 10.10.2016)
1425
            if (!synRelation.equals(oldSynonym)){
1426
                result.setError();
1427
            }
1428
        }
1429

    
1430
        result.addUpdatedObject(oldTaxon);
1431
        result.addUpdatedObject(newTaxon);
1432
        saveOrUpdate(oldTaxon);
1433
        saveOrUpdate(newTaxon);
1434

    
1435
        return result;
1436
    }
1437

    
1438
    @Override
1439
    public <T extends TaxonBase> List<UuidAndTitleCache<T>> getUuidAndTitleCache(Class<T> clazz, Integer limit, String pattern) {
1440
        return dao.getUuidAndTitleCache(clazz, limit, pattern);
1441
    }
1442

    
1443
    @Override
1444
    public Pager<SearchResult<TaxonBase>> findByFullText(
1445
            Class<? extends TaxonBase> clazz, String queryString,
1446
            Classification classification, List<Language> languages,
1447
            boolean highlightFragments, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
1448

    
1449

    
1450
        LuceneSearch luceneSearch = prepareFindByFullTextSearch(clazz, queryString, classification, languages, highlightFragments, null);
1451

    
1452
        // --- execute search
1453
        TopGroups<BytesRef> topDocsResultSet;
1454
        try {
1455
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
1456
        } catch (ParseException e) {
1457
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
1458
            luceneParseException.setStackTrace(e.getStackTrace());
1459
            throw luceneParseException;
1460
        }
1461

    
1462
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1463
        idFieldMap.put(CdmBaseType.TAXON, "id");
1464

    
1465
        // ---  initialize taxa, thighlight matches ....
1466
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1467
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1468
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1469

    
1470
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
1471
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
1472
    }
1473

    
1474
    @Override
1475
    public Pager<SearchResult<TaxonBase>> findByDistribution(List<NamedArea> areaFilter, List<PresenceAbsenceTerm> statusFilter,
1476
            Classification classification,
1477
            Integer pageSize, Integer pageNumber,
1478
            List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
1479

    
1480
        LuceneSearch luceneSearch = prepareByDistributionSearch(areaFilter, statusFilter, classification);
1481

    
1482
        // --- execute search
1483
        TopGroups<BytesRef> topDocsResultSet;
1484
        try {
1485
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
1486
        } catch (ParseException e) {
1487
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
1488
            luceneParseException.setStackTrace(e.getStackTrace());
1489
            throw luceneParseException;
1490
        }
1491

    
1492
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1493
        idFieldMap.put(CdmBaseType.TAXON, "id");
1494

    
1495
        // ---  initialize taxa, thighlight matches ....
1496
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1497
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1498
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1499

    
1500
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
1501
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
1502
    }
1503

    
1504
    /**
1505
     * @param clazz
1506
     * @param queryString
1507
     * @param classification
1508
     * @param languages
1509
     * @param highlightFragments
1510
     * @param sortFields TODO
1511
     * @param directorySelectClass
1512
     * @return
1513
     */
1514
    protected LuceneSearch prepareFindByFullTextSearch(Class<? extends CdmBase> clazz, String queryString, Classification classification, List<Language> languages,
1515
            boolean highlightFragments, SortField[] sortFields) {
1516
        Builder finalQueryBuilder = new Builder();
1517
        Builder textQueryBuilder = new Builder();
1518

    
1519
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1520
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1521

    
1522
        if(sortFields == null){
1523
            sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING,  false)};
1524
        }
1525
        luceneSearch.setSortFields(sortFields);
1526

    
1527
        // ---- search criteria
1528
        luceneSearch.setCdmTypRestriction(clazz);
1529

    
1530
        if(!queryString.isEmpty() && !queryString.equals("*") && !queryString.equals("?") ) {
1531
            textQueryBuilder.add(taxonBaseQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
1532
            textQueryBuilder.add(taxonBaseQueryFactory.newDefinedTermQuery("name.rank", queryString, languages), Occur.SHOULD);
1533
        }
1534

    
1535
        BooleanQuery textQuery = textQueryBuilder.build();
1536
        if(textQuery.clauses().size() > 0) {
1537
            finalQueryBuilder.add(textQuery, Occur.MUST);
1538
        }
1539

    
1540

    
1541
        if(classification != null){
1542
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery("taxonNodes.classification.id", classification), Occur.MUST);
1543
        }
1544
        luceneSearch.setQuery(finalQueryBuilder.build());
1545

    
1546
        if(highlightFragments){
1547
            luceneSearch.setHighlightFields(taxonBaseQueryFactory.getTextFieldNamesAsArray());
1548
        }
1549
        return luceneSearch;
1550
    }
1551

    
1552
    /**
1553
     * Uses org.apache.lucene.search.join.JoinUtil for query time joining, alternatively
1554
     * the BlockJoinQuery could be used. The latter might be more memory save but has the
1555
     * drawback of requiring to do the join an indexing time.
1556
     * see  http://dev.e-taxonomy.eu/trac/wiki/LuceneNotes#JoinsinLucene for more information on this.
1557
     *
1558
     * Joins TaxonRelationShip with Taxon depending on the direction of the given edge:
1559
     * <ul>
1560
     * <li>direct, everted: {@link Direction.relatedTo}: TaxonRelationShip.relatedTo.id --&gt; Taxon.id </li>
1561
     * <li>inverse: {@link Direction.relatedFrom}:  TaxonRelationShip.relatedFrom.id --&gt; Taxon.id </li>
1562
     * <ul>
1563
     * @param queryString
1564
     * @param classification
1565
     * @param languages
1566
     * @param highlightFragments
1567
     * @param sortFields TODO
1568
     *
1569
     * @return
1570
     * @throws IOException
1571
     */
1572
    protected LuceneSearch prepareFindByTaxonRelationFullTextSearch(TaxonRelationshipEdge edge, String queryString, Classification classification, List<Language> languages,
1573
            boolean highlightFragments, SortField[] sortFields) throws IOException {
1574

    
1575
        String fromField;
1576
        String queryTermField;
1577
        String toField = "id"; // TaxonBase.uuid
1578

    
1579
        if(edge.isBidirectional()){
1580
            throw new RuntimeException("Bidirectional joining not supported!");
1581
        }
1582
        if(edge.isEvers()){
1583
            fromField = "relatedFrom.id";
1584
            queryTermField = "relatedFrom.titleCache";
1585
        } else if(edge.isInvers()) {
1586
            fromField = "relatedTo.id";
1587
            queryTermField = "relatedTo.titleCache";
1588
        } else {
1589
            throw new RuntimeException("Invalid direction: " + edge.getDirections());
1590
        }
1591

    
1592
        Builder finalQueryBuilder = new Builder();
1593

    
1594
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1595
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1596

    
1597
        Builder joinFromQueryBuilder = new Builder();
1598
        joinFromQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(queryTermField, queryString), Occur.MUST);
1599
        joinFromQueryBuilder.add(taxonBaseQueryFactory.newEntityIdsQuery("type.id", edge.getTaxonRelationshipTypes()), Occur.MUST);
1600

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

    
1603
        if(sortFields == null){
1604
            sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING,  false)};
1605
        }
1606
        luceneSearch.setSortFields(sortFields);
1607

    
1608
        finalQueryBuilder.add(joinQuery, Occur.MUST);
1609

    
1610
        if(classification != null){
1611
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery("taxonNodes.classification.id", classification), Occur.MUST);
1612
        }
1613
        luceneSearch.setQuery(finalQueryBuilder.build());
1614

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

    
1621
    @Override
1622
    public Pager<SearchResult<TaxonBase>> findTaxaAndNamesByFullText(
1623
            EnumSet<TaxaAndNamesSearchMode> searchModes, String queryString, Classification classification,
1624
            Set<NamedArea> namedAreas, Set<PresenceAbsenceTerm> distributionStatus, List<Language> languages,
1625
            boolean highlightFragments, Integer pageSize,
1626
            Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths)
1627
            throws IOException, LuceneParseException, LuceneMultiSearchException {
1628

    
1629
        // FIXME: allow taxonomic ordering
1630
        //  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";
1631
        // this require building a special sort column by a special classBridge
1632
        if(highlightFragments){
1633
            logger.warn("findTaxaAndNamesByFullText() : fragment highlighting is " +
1634
                    "currently not fully supported by this method and thus " +
1635
                    "may not work with common names and misapplied names.");
1636
        }
1637

    
1638
        // convert sets to lists
1639
        List<NamedArea> namedAreaList = null;
1640
        List<PresenceAbsenceTerm>distributionStatusList = null;
1641
        if(namedAreas != null){
1642
            namedAreaList = new ArrayList<>(namedAreas.size());
1643
            namedAreaList.addAll(namedAreas);
1644
        }
1645
        if(distributionStatus != null){
1646
            distributionStatusList = new ArrayList<>(distributionStatus.size());
1647
            distributionStatusList.addAll(distributionStatus);
1648
        }
1649

    
1650
        // set default if parameter is null
1651
        if(searchModes == null){
1652
            searchModes = EnumSet.of(TaxaAndNamesSearchMode.doTaxa);
1653
        }
1654

    
1655
        // set sort order and thus override any sort orders which may have been
1656
        // defined by prepare*Search methods
1657
        if(orderHints == null){
1658
            orderHints = OrderHint.NOMENCLATURAL_SORT_ORDER.asList();
1659
        }
1660
        SortField[] sortFields = new SortField[orderHints.size()];
1661
        int i = 0;
1662
        for(OrderHint oh : orderHints){
1663
            sortFields[i++] = oh.toSortField();
1664
        }
1665
//        SortField[] sortFields = new SortField[]{SortField.FIELD_SCORE, new SortField("id", SortField.STRING, false)};
1666
//        SortField[] sortFields = new SortField[]{new SortField(NomenclaturalSortOrderBrigde.NAME_SORT_FIELD_NAME, SortField.STRING, false)};
1667

    
1668

    
1669
        boolean addDistributionFilter = namedAreas != null && namedAreas.size() > 0;
1670

    
1671
        List<LuceneSearch> luceneSearches = new ArrayList<>();
1672
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1673

    
1674
        /*
1675
          ======== filtering by distribution , HOWTO ========
1676

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

    
1682

    
1683
          3. how does it work in spatial?
1684
          see
1685
           - http://www.nsshutdown.com/projects/lucene/whitepaper/locallucene_v2.html
1686
           - http://www.infoq.com/articles/LuceneSpatialSupport
1687
           - http://www.mhaller.de/archives/156-Spatial-search-with-Lucene.html
1688
          ------------------------------------------------------------------------
1689

    
1690
          filter strategies:
1691
          A) use a separate distribution filter per index sub-query/search:
1692
           - byTaxonSyonym (query TaxaonBase):
1693
               use a join area filter (Distribution -> TaxonBase)
1694
           - byCommonName (query DescriptionElementBase): use an area filter on
1695
               DescriptionElementBase !!! PROBLEM !!!
1696
               This cannot work since the distributions are different entities than the
1697
               common names and thus these are different lucene documents.
1698
           - byMisaplliedNames (join query TaxonRelationship -> TaxaonBase):
1699
               use a join area filter (Distribution -> TaxonBase)
1700

    
1701
          B) use a common distribution filter for all index sub-query/searches:
1702
           - use a common join area filter (Distribution -> TaxonBase)
1703
           - also implement the byCommonName as join query (CommonName -> TaxonBase)
1704
           PROBLEM in this case: we are losing the fragment highlighting for the
1705
           common names, since the returned documents are always TaxonBases
1706
        */
1707

    
1708
        /* The QueryFactory for creating filter queries on Distributions should
1709
         * The query factory used for the common names query cannot be reused
1710
         * for this case, since we want to only record the text fields which are
1711
         * actually used in the primary query
1712
         */
1713
        QueryFactory distributionFilterQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Distribution.class);
1714

    
1715
        Builder multiIndexByAreaFilterBuilder = new Builder();
1716

    
1717
        // search for taxa or synonyms
1718
        if(searchModes.contains(TaxaAndNamesSearchMode.doTaxa) || searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)) {
1719
            Class taxonBaseSubclass = TaxonBase.class;
1720
            if(searchModes.contains(TaxaAndNamesSearchMode.doTaxa) && !searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
1721
                taxonBaseSubclass = Taxon.class;
1722
            } else if (!searchModes.contains(TaxaAndNamesSearchMode.doTaxa) && searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)) {
1723
                taxonBaseSubclass = Synonym.class;
1724
            }
1725
            luceneSearches.add(prepareFindByFullTextSearch(taxonBaseSubclass, queryString, classification, languages, highlightFragments, sortFields));
1726
            idFieldMap.put(CdmBaseType.TAXON, "id");
1727
            /* A) does not work!!!!
1728
            if(addDistributionFilter){
1729
                // in this case we need a filter which uses a join query
1730
                // to get the TaxonBase documents for the DescriptionElementBase documents
1731
                // which are matching the areas in question
1732
                Query taxonAreaJoinQuery = createByDistributionJoinQuery(
1733
                        namedAreaList,
1734
                        distributionStatusList,
1735
                        distributionFilterQueryFactory
1736
                        );
1737
                multiIndexByAreaFilter.add(new QueryWrapperFilter(taxonAreaJoinQuery), Occur.SHOULD);
1738
            }
1739
            */
1740
            if(addDistributionFilter && searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
1741
                // add additional area filter for synonyms
1742
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1743
                String toField = "accTaxon.id"; // id in TaxonBase index (is multivalued)
1744

    
1745
                //TODO replace by createByDistributionJoinQuery
1746
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
1747
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class, fromField, true, byDistributionQuery, toField, Taxon.class, ScoreMode.None);
1748
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1749

    
1750
            }
1751
        }
1752

    
1753
        // search by CommonTaxonName
1754
        if(searchModes.contains(TaxaAndNamesSearchMode.doTaxaByCommonNames)) {
1755
            // B)
1756
            QueryFactory descriptionElementQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(DescriptionElementBase.class);
1757
            Query byCommonNameJoinQuery = descriptionElementQueryFactory.newJoinQuery(
1758
                    CommonTaxonName.class,
1759
                    "inDescription.taxon.id",
1760
                    true,
1761
                    QueryFactory.addTypeRestriction(
1762
                                createByDescriptionElementFullTextQuery(queryString, classification, null, languages, descriptionElementQueryFactory)
1763
                                , CommonTaxonName.class
1764
                                ).build(), "id", null, ScoreMode.Max);
1765
            logger.debug("byCommonNameJoinQuery: " + byCommonNameJoinQuery.toString());
1766
            LuceneSearch byCommonNameSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
1767
            byCommonNameSearch.setCdmTypRestriction(Taxon.class);
1768
            byCommonNameSearch.setQuery(byCommonNameJoinQuery);
1769
            byCommonNameSearch.setSortFields(sortFields);
1770

    
1771
            DuplicateFilter df = new DuplicateFilter("inDescription.taxon.id");
1772
            Set<String> results=new HashSet<>();
1773
//            ScoreDoc[] hits = searcher.search(tq,df, 1000).scoreDocs;
1774
//
1775
//            byCommonNameSearch.setFilter(df);
1776
            idFieldMap.put(CdmBaseType.TAXON, "id");
1777

    
1778
            luceneSearches.add(byCommonNameSearch);
1779

    
1780
            /* A) does not work!!!!
1781
            luceneSearches.add(
1782
                    prepareByDescriptionElementFullTextSearch(CommonTaxonName.class,
1783
                            queryString, classification, null, languages, highlightFragments)
1784
                        );
1785
            idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
1786
            if(addDistributionFilter){
1787
                // in this case we are able to use DescriptionElementBase documents
1788
                // which are matching the areas in question directly
1789
                BooleanQuery byDistributionQuery = createByDistributionQuery(
1790
                        namedAreaList,
1791
                        distributionStatusList,
1792
                        distributionFilterQueryFactory
1793
                        );
1794
                multiIndexByAreaFilter.add(new QueryWrapperFilter(byDistributionQuery), Occur.SHOULD);
1795
            } */
1796
        }
1797

    
1798
        // search by misapplied names
1799
        if(searchModes.contains(TaxaAndNamesSearchMode.doMisappliedNames)) {
1800
            // NOTE:
1801
            // prepareFindByTaxonRelationFullTextSearch() is making use of JoinUtil.createJoinQuery()
1802
            // which allows doing query time joins
1803
            // finds the misapplied name (Taxon B) which is an misapplication for
1804
            // a related Taxon A.
1805
            //
1806
            luceneSearches.add(prepareFindByTaxonRelationFullTextSearch(
1807
                    new TaxonRelationshipEdge(TaxonRelationshipType.allMisappliedNameTypes(), Direction.relatedTo),
1808
                    queryString, classification, languages, highlightFragments, sortFields));
1809
            idFieldMap.put(CdmBaseType.TAXON, "id");
1810

    
1811
            if(addDistributionFilter){
1812
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1813

    
1814
                /*
1815
                 * Here i was facing wired and nasty bug which took me bugging be really for hours until I found this solution.
1816
                 * Maybe this is a bug in java itself java.
1817
                 *
1818
                 * When the string toField is constructed by using the expression TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString()
1819
                 * directly:
1820
                 *
1821
                 *    String toField = "relation." + TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString() +".to.id";
1822
                 *
1823
                 * The byDistributionQuery fails, however when the uuid is first stored in another string variable the query
1824
                 * will execute as expected:
1825
                 *
1826
                 *    String misappliedNameForUuid = TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString();
1827
                 *    String toField = "relation." + misappliedNameForUuid +".to.id";
1828
                 *
1829
                 * Comparing both strings by the String.equals method returns true, so both String are identical.
1830
                 *
1831
                 * The bug occurs when running eu.etaxonomy.cdm.api.service.TaxonServiceSearchTest in eclipse and in maven and seems to to be
1832
                 * 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)
1833
                 * The bug is persistent after a reboot of the development computer.
1834
                 */
1835
//                String misappliedNameForUuid = TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString();
1836
//                String toField = "relation." + misappliedNameForUuid +".to.id";
1837
                String toField = "relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id";
1838
//                System.out.println("relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id".equals("relation." + misappliedNameForUuid +".to.id") ? " > identical" : " > different");
1839
//                System.out.println("relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id".equals("relation." + TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString() +".to.id") ? " > identical" : " > different");
1840

    
1841
                //TODO replace by createByDistributionJoinQuery
1842
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
1843
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class, fromField, true, byDistributionQuery, toField, null, ScoreMode.None);
1844

    
1845
//                debug code for bug described above
1846
                //does not compile anymore since changing from lucene 3.6.2 to lucene 4.10+
1847
//                DocIdSet filterMatchSet = filter.getDocIdSet(luceneIndexToolProvider.getIndexReaderFor(Taxon.class));
1848
//                System.err.println(DocIdBitSetPrinter.docsAsString(filterMatchSet, 100));
1849

    
1850
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1851
            }
1852
        }
1853

    
1854
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider,
1855
                luceneSearches.toArray(new LuceneSearch[luceneSearches.size()]));
1856

    
1857

    
1858
        if(addDistributionFilter){
1859

    
1860
            // B)
1861
            // in this case we need a filter which uses a join query
1862
            // to get the TaxonBase documents for the DescriptionElementBase documents
1863
            // which are matching the areas in question
1864
            //
1865
            // for toTaxa, doByCommonName
1866
            Query taxonAreaJoinQuery = createByDistributionJoinQuery(
1867
                    namedAreaList,
1868
                    distributionStatusList,
1869
                    distributionFilterQueryFactory,
1870
                    Taxon.class, true
1871
                    );
1872
            multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1873
        }
1874

    
1875
        if (addDistributionFilter){
1876
            multiSearch.setFilter(multiIndexByAreaFilterBuilder.build());
1877
        }
1878

    
1879

    
1880
        // --- execute search
1881
        TopGroups<BytesRef> topDocsResultSet;
1882
        try {
1883
            topDocsResultSet = multiSearch.executeSearch(pageSize, pageNumber);
1884
        } catch (ParseException e) {
1885
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
1886
            luceneParseException.setStackTrace(e.getStackTrace());
1887
            throw luceneParseException;
1888
        }
1889

    
1890
        // --- initialize taxa, highlight matches ....
1891
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
1892

    
1893

    
1894
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1895
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1896

    
1897
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
1898
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
1899
    }
1900

    
1901
    /**
1902
     * @param namedAreaList at least one area must be in the list
1903
     * @param distributionStatusList optional
1904
     * @param toType toType
1905
     *      Optional parameter. Only used for debugging to print the toType documents
1906
     * @param asFilter TODO
1907
     * @return
1908
     * @throws IOException
1909
     */
1910
    protected Query createByDistributionJoinQuery(
1911
            List<NamedArea> namedAreaList,
1912
            List<PresenceAbsenceTerm> distributionStatusList,
1913
            QueryFactory queryFactory, Class<? extends CdmBase> toType, boolean asFilter
1914
            ) throws IOException {
1915

    
1916
        String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1917
        String toField = "id"; // id in toType usually this is the TaxonBase index
1918

    
1919
        BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, queryFactory);
1920

    
1921
        ScoreMode scoreMode = asFilter ?  ScoreMode.None : ScoreMode.Max;
1922

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

    
1925
        return taxonAreaJoinQuery;
1926
    }
1927

    
1928
    /**
1929
     * @param namedAreaList
1930
     * @param distributionStatusList
1931
     * @param queryFactory
1932
     * @return
1933
     */
1934
    private BooleanQuery createByDistributionQuery(List<NamedArea> namedAreaList,
1935
            List<PresenceAbsenceTerm> distributionStatusList, QueryFactory queryFactory) {
1936
        Builder areaQueryBuilder = new Builder();
1937
        // area field from Distribution
1938
        areaQueryBuilder.add(queryFactory.newEntityIdsQuery("area.id", namedAreaList), Occur.MUST);
1939

    
1940
        // status field from Distribution
1941
        if(distributionStatusList != null && distributionStatusList.size() > 0){
1942
            areaQueryBuilder.add(queryFactory.newEntityIdsQuery("status.id", distributionStatusList), Occur.MUST);
1943
        }
1944

    
1945
        BooleanQuery areaQuery = areaQueryBuilder.build();
1946
        logger.debug("createByDistributionQuery() query: " + areaQuery.toString());
1947
        return areaQuery;
1948
    }
1949

    
1950
    /**
1951
     * This method has been primarily created for testing the area join query but might
1952
     * also be useful in other situations
1953
     *
1954
     * @param namedAreaList
1955
     * @param distributionStatusList
1956
     * @param classification
1957
     * @param highlightFragments
1958
     * @return
1959
     * @throws IOException
1960
     */
1961
    protected LuceneSearch prepareByDistributionSearch(
1962
            List<NamedArea> namedAreaList, List<PresenceAbsenceTerm> distributionStatusList,
1963
            Classification classification) throws IOException {
1964

    
1965
        Builder finalQueryBuilder = new Builder();
1966

    
1967
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
1968

    
1969
        // FIXME is this query factory using the wrong type?
1970
        QueryFactory taxonQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Taxon.class);
1971

    
1972
        SortField[] sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING, false)};
1973
        luceneSearch.setSortFields(sortFields);
1974

    
1975

    
1976
        Query byAreaQuery = createByDistributionJoinQuery(namedAreaList, distributionStatusList, taxonQueryFactory, null, false);
1977

    
1978
        finalQueryBuilder.add(byAreaQuery, Occur.MUST);
1979

    
1980
        if(classification != null){
1981
            finalQueryBuilder.add(taxonQueryFactory.newEntityIdQuery("taxonNodes.classification.id", classification), Occur.MUST);
1982
        }
1983
        BooleanQuery finalQuery = finalQueryBuilder.build();
1984
        logger.info("prepareByAreaSearch() query: " + finalQuery.toString());
1985
        luceneSearch.setQuery(finalQuery);
1986

    
1987
        return luceneSearch;
1988
    }
1989

    
1990
    @Override
1991
    public Pager<SearchResult<TaxonBase>> findByDescriptionElementFullText(
1992
            Class<? extends DescriptionElementBase> clazz, String queryString,
1993
            Classification classification, List<Feature> features, List<Language> languages,
1994
            boolean highlightFragments, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
1995

    
1996

    
1997
        LuceneSearch luceneSearch = prepareByDescriptionElementFullTextSearch(clazz, queryString, classification, features, languages, highlightFragments);
1998

    
1999
        // --- execute search
2000
        TopGroups<BytesRef> topDocsResultSet;
2001
        try {
2002
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
2003
        } catch (ParseException e) {
2004
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2005
            luceneParseException.setStackTrace(e.getStackTrace());
2006
            throw luceneParseException;
2007
        }
2008

    
2009
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
2010
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2011

    
2012
        // --- initialize taxa, highlight matches ....
2013
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
2014
        @SuppressWarnings("rawtypes")
2015
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2016
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2017

    
2018
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
2019
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
2020

    
2021
    }
2022

    
2023

    
2024
    @Override
2025
    public Pager<SearchResult<TaxonBase>> findByEverythingFullText(String queryString,
2026
            Classification classification, List<Language> languages, boolean highlightFragments,
2027
            Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException, LuceneMultiSearchException {
2028

    
2029
        LuceneSearch luceneSearchByDescriptionElement = prepareByDescriptionElementFullTextSearch(null, queryString, classification, null, languages, highlightFragments);
2030
        LuceneSearch luceneSearchByTaxonBase = prepareFindByFullTextSearch(null, queryString, classification, languages, highlightFragments, null);
2031

    
2032
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider, luceneSearchByDescriptionElement, luceneSearchByTaxonBase);
2033

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

    
2044
        // --- initialize taxa, highlight matches ....
2045
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
2046

    
2047
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
2048
        idFieldMap.put(CdmBaseType.TAXON, "id");
2049
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2050

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

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

    
2057
    }
2058

    
2059

    
2060
    /**
2061
     * @param clazz
2062
     * @param queryString
2063
     * @param classification
2064
     * @param features
2065
     * @param languages
2066
     * @param highlightFragments
2067
     * @param directorySelectClass
2068
     * @return
2069
     */
2070
    protected LuceneSearch prepareByDescriptionElementFullTextSearch(Class<? extends CdmBase> clazz,
2071
            String queryString, Classification classification, List<Feature> features,
2072
            List<Language> languages, boolean highlightFragments) {
2073

    
2074
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, DescriptionElementBase.class);
2075
        QueryFactory descriptionElementQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(DescriptionElementBase.class);
2076

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

    
2079
        BooleanQuery finalQuery = createByDescriptionElementFullTextQuery(queryString, classification, features,
2080
                languages, descriptionElementQueryFactory);
2081

    
2082
        luceneSearch.setSortFields(sortFields);
2083
        luceneSearch.setCdmTypRestriction(clazz);
2084
        luceneSearch.setQuery(finalQuery);
2085
        if(highlightFragments){
2086
            luceneSearch.setHighlightFields(descriptionElementQueryFactory.getTextFieldNamesAsArray());
2087
        }
2088

    
2089
        return luceneSearch;
2090
    }
2091

    
2092
    /**
2093
     * @param queryString
2094
     * @param classification
2095
     * @param features
2096
     * @param languages
2097
     * @param descriptionElementQueryFactory
2098
     * @return
2099
     */
2100
    private BooleanQuery createByDescriptionElementFullTextQuery(String queryString, Classification classification,
2101
            List<Feature> features, List<Language> languages, QueryFactory descriptionElementQueryFactory) {
2102
        Builder finalQueryBuilder = new Builder();
2103
        Builder textQueryBuilder = new Builder();
2104
        textQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
2105

    
2106
        // common name
2107
        Builder nameQueryBuilder = new Builder();
2108
        if(languages == null || languages.size() == 0){
2109
            nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2110
        } else {
2111
            Builder languageSubQueryBuilder = new Builder();
2112
            for(Language lang : languages){
2113
                languageSubQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("language.uuid",  lang.getUuid().toString(), false), Occur.SHOULD);
2114
            }
2115
            nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2116
            nameQueryBuilder.add(languageSubQueryBuilder.build(), Occur.MUST);
2117
        }
2118
        textQueryBuilder.add(nameQueryBuilder.build(), Occur.SHOULD);
2119

    
2120

    
2121
        // text field from TextData
2122
        textQueryBuilder.add(descriptionElementQueryFactory.newMultilanguageTextQuery("text", queryString, languages), Occur.SHOULD);
2123

    
2124
        // --- TermBase fields - by representation ----
2125
        // state field from CategoricalData
2126
        textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.state", queryString, languages), Occur.SHOULD);
2127

    
2128
        // state field from CategoricalData
2129
        textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.modifyingText", queryString, languages), Occur.SHOULD);
2130

    
2131
        // area field from Distribution
2132
        textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("area", queryString, languages), Occur.SHOULD);
2133

    
2134
        // status field from Distribution
2135
        textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("status", queryString, languages), Occur.SHOULD);
2136

    
2137
        finalQueryBuilder.add(textQueryBuilder.build(), Occur.MUST);
2138
        // --- classification ----
2139

    
2140
        if(classification != null){
2141
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityIdQuery("inDescription.taxon.taxonNodes.classification.id", classification), Occur.MUST);
2142
        }
2143

    
2144
        // --- IdentifieableEntity fields - by uuid
2145
        if(features != null && features.size() > 0 ){
2146
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityUuidsQuery("feature.uuid", features), Occur.MUST);
2147
        }
2148

    
2149
        // the description must be associated with a taxon
2150
        finalQueryBuilder.add(descriptionElementQueryFactory.newIsNotNullQuery("inDescription.taxon.id"), Occur.MUST);
2151

    
2152
        BooleanQuery finalQuery = finalQueryBuilder.build();
2153
        logger.info("prepareByDescriptionElementFullTextSearch() query: " + finalQuery.toString());
2154
        return finalQuery;
2155
    }
2156

    
2157
    /**
2158
     * DefinedTerm representations and MultilanguageString maps are stored in the Lucene index by the {@link DefinedTermBaseClassBridge}
2159
     * and {@link MultilanguageTextFieldBridge } in a consistent way. One field per language and also in one additional field for all languages.
2160
     * This method is a convenient means to retrieve a Lucene query string for such the fields.
2161
     *
2162
     * @param name name of the term field as in the Lucene index. Must be field created by {@link DefinedTermBaseClassBridge}
2163
     * or {@link MultilanguageTextFieldBridge }
2164
     * @param languages the languages to search for exclusively. Can be <code>null</code> to search in all languages
2165
     * @param stringBuilder a StringBuilder to be reused, if <code>null</code> a new StringBuilder will be instantiated and is returned
2166
     * @return the StringBuilder given a parameter or a new one if the stringBuilder parameter was null.
2167
     *
2168
     * TODO move to utiliy class !!!!!!!!
2169
     */
2170
    private StringBuilder appendLocalizedFieldQuery(String name, List<Language> languages, StringBuilder stringBuilder) {
2171

    
2172
        if(stringBuilder == null){
2173
            stringBuilder = new StringBuilder();
2174
        }
2175
        if(languages == null || languages.size() == 0){
2176
            stringBuilder.append(name + ".ALL:(%1$s) ");
2177
        } else {
2178
            for(Language lang : languages){
2179
                stringBuilder.append(name + "." + lang.getUuid().toString() + ":(%1$s) ");
2180
            }
2181
        }
2182
        return stringBuilder;
2183
    }
2184

    
2185
    @Override
2186
    public List<Synonym> createInferredSynonyms(Taxon taxon, Classification classification, SynonymType type, boolean doWithMisappliedNames){
2187
        List <Synonym> inferredSynonyms = new ArrayList<>();
2188
        List<Synonym> inferredSynonymsToBeRemoved = new ArrayList<>();
2189

    
2190
        HashMap <UUID, IZoologicalName> zooHashMap = new HashMap<>();
2191

    
2192

    
2193
        UUID nameUuid= taxon.getName().getUuid();
2194
        IZoologicalName taxonName = getZoologicalName(nameUuid, zooHashMap);
2195
        String epithetOfTaxon = null;
2196
        String infragenericEpithetOfTaxon = null;
2197
        String infraspecificEpithetOfTaxon = null;
2198
        if (taxonName.isSpecies()){
2199
             epithetOfTaxon= taxonName.getSpecificEpithet();
2200
        } else if (taxonName.isInfraGeneric()){
2201
            infragenericEpithetOfTaxon = taxonName.getInfraGenericEpithet();
2202
        } else if (taxonName.isInfraSpecific()){
2203
            infraspecificEpithetOfTaxon = taxonName.getInfraSpecificEpithet();
2204
        }
2205
        String genusOfTaxon = taxonName.getGenusOrUninomial();
2206
        Set<TaxonNode> nodes = taxon.getTaxonNodes();
2207
        List<String> taxonNames = new ArrayList<>();
2208

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

    
2213
            if (node.getClassification().equals(classification)){
2214
                if (!node.isTopmostNode()){
2215
                    TaxonNode parent = node.getParent();
2216
                    parent = CdmBase.deproxy(parent);
2217
                    TaxonName parentName =  parent.getTaxon().getName();
2218
                    IZoologicalName zooParentName = CdmBase.deproxy(parentName);
2219
                    Taxon parentTaxon = CdmBase.deproxy(parent.getTaxon());
2220
                    Rank rankOfTaxon = taxonName.getRank();
2221

    
2222

    
2223
                    //create inferred synonyms for species, subspecies
2224
                    if ((parentName.isGenus() || parentName.isSpecies() || parentName.getRank().equals(Rank.SUBGENUS())) ){
2225

    
2226
                        Synonym inferredEpithet = null;
2227
                        Synonym inferredGenus = null;
2228
                        Synonym potentialCombination = null;
2229

    
2230
                        List<String> propertyPaths = new ArrayList<>();
2231
                        propertyPaths.add("synonym");
2232
                        propertyPaths.add("synonym.name");
2233
                        List<OrderHint> orderHintsSynonyms = new ArrayList<>();
2234
                        orderHintsSynonyms.add(new OrderHint("titleCache", SortOrder.ASCENDING));
2235

    
2236
                        List<Synonym> synonyMsOfParent = dao.getSynonyms(parentTaxon, SynonymType.HETEROTYPIC_SYNONYM_OF(), null, null,orderHintsSynonyms,propertyPaths);
2237
                        List<Synonym> synonymsOfTaxon= dao.getSynonyms(taxon, SynonymType.HETEROTYPIC_SYNONYM_OF(),
2238
                                null, null,orderHintsSynonyms,propertyPaths);
2239

    
2240
                        List<TaxonRelationship> taxonRelListParent = new ArrayList<>();
2241
                        List<TaxonRelationship> taxonRelListTaxon = new ArrayList<>();
2242
                        if (doWithMisappliedNames){
2243
                            List<OrderHint> orderHintsMisapplied = new ArrayList<>();
2244
                            orderHintsMisapplied.add(new OrderHint("relatedFrom.titleCache", SortOrder.ASCENDING));
2245
                            taxonRelListParent = dao.getTaxonRelationships(parentTaxon, TaxonRelationshipType.MISAPPLIED_NAME_FOR(),
2246
                                    null, null, orderHintsMisapplied, propertyPaths, Direction.relatedTo);
2247
                            taxonRelListTaxon = dao.getTaxonRelationships(taxon, TaxonRelationshipType.MISAPPLIED_NAME_FOR(),
2248
                                    null, null, orderHintsMisapplied, propertyPaths, Direction.relatedTo);
2249
                        }
2250

    
2251
                        if (type.equals(SynonymType.INFERRED_EPITHET_OF())){
2252
                            for (Synonym synonymRelationOfParent:synonyMsOfParent){
2253

    
2254
                                inferredEpithet = createInferredEpithets(taxon,
2255
                                        zooHashMap, taxonName, epithetOfTaxon,
2256
                                        infragenericEpithetOfTaxon,
2257
                                        infraspecificEpithetOfTaxon,
2258
                                        taxonNames, parentName,
2259
                                        synonymRelationOfParent);
2260

    
2261
                                inferredSynonyms.add(inferredEpithet);
2262
                                zooHashMap.put(inferredEpithet.getName().getUuid(), inferredEpithet.getName());
2263
                                taxonNames.add(inferredEpithet.getName().getNameCache());
2264
                            }
2265

    
2266
                            if (doWithMisappliedNames){
2267

    
2268
                                for (TaxonRelationship taxonRelationship: taxonRelListParent){
2269
                                     Taxon misappliedName = taxonRelationship.getFromTaxon();
2270

    
2271
                                     inferredEpithet = createInferredEpithets(taxon,
2272
                                             zooHashMap, taxonName, epithetOfTaxon,
2273
                                             infragenericEpithetOfTaxon,
2274
                                             infraspecificEpithetOfTaxon,
2275
                                             taxonNames, parentName,
2276
                                             misappliedName);
2277

    
2278
                                    inferredSynonyms.add(inferredEpithet);
2279
                                    zooHashMap.put(inferredEpithet.getName().getUuid(), inferredEpithet.getName());
2280
                                    taxonNames.add(inferredEpithet.getName().getNameCache());
2281
                                }
2282
                            }
2283

    
2284
                            if (!taxonNames.isEmpty()){
2285
                            List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2286
                            IZoologicalName name;
2287
                            if (!synNotInCDM.isEmpty()){
2288
                                inferredSynonymsToBeRemoved.clear();
2289

    
2290
                                for (Synonym syn :inferredSynonyms){
2291
                                    name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2292
                                    if (!synNotInCDM.contains(name.getNameCache())){
2293
                                        inferredSynonymsToBeRemoved.add(syn);
2294
                                    }
2295
                                }
2296

    
2297
                                // Remove identified Synonyms from inferredSynonyms
2298
                                for (Synonym synonym : inferredSynonymsToBeRemoved) {
2299
                                    inferredSynonyms.remove(synonym);
2300
                                }
2301
                            }
2302
                        }
2303

    
2304
                    }else if (type.equals(SynonymType.INFERRED_GENUS_OF())){
2305

    
2306
                        for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2307

    
2308
                            inferredGenus = createInferredGenus(taxon,
2309
                                    zooHashMap, taxonName, epithetOfTaxon,
2310
                                    genusOfTaxon, taxonNames, zooParentName, synonymRelationOfTaxon);
2311

    
2312
                            inferredSynonyms.add(inferredGenus);
2313
                            zooHashMap.put(inferredGenus.getName().getUuid(), inferredGenus.getName());
2314
                            taxonNames.add(inferredGenus.getName().getNameCache());
2315
                        }
2316

    
2317
                        if (doWithMisappliedNames){
2318

    
2319
                            for (TaxonRelationship taxonRelationship: taxonRelListTaxon){
2320
                                Taxon misappliedName = taxonRelationship.getFromTaxon();
2321
                                inferredGenus = createInferredGenus(taxon, zooHashMap, taxonName, infraspecificEpithetOfTaxon, genusOfTaxon, taxonNames, zooParentName,  misappliedName);
2322

    
2323
                                inferredSynonyms.add(inferredGenus);
2324
                                zooHashMap.put(inferredGenus.getName().getUuid(), inferredGenus.getName());
2325
                                 taxonNames.add(inferredGenus.getName().getNameCache());
2326
                            }
2327
                        }
2328

    
2329

    
2330
                        if (!taxonNames.isEmpty()){
2331
                            List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2332
                            IZoologicalName name;
2333
                            if (!synNotInCDM.isEmpty()){
2334
                                inferredSynonymsToBeRemoved.clear();
2335

    
2336
                                for (Synonym syn :inferredSynonyms){
2337
                                    name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2338
                                    if (!synNotInCDM.contains(name.getNameCache())){
2339
                                        inferredSynonymsToBeRemoved.add(syn);
2340
                                    }
2341
                                }
2342

    
2343
                                // Remove identified Synonyms from inferredSynonyms
2344
                                for (Synonym synonym : inferredSynonymsToBeRemoved) {
2345
                                    inferredSynonyms.remove(synonym);
2346
                                }
2347
                            }
2348
                        }
2349

    
2350
                    }else if (type.equals(SynonymType.POTENTIAL_COMBINATION_OF())){
2351

    
2352
                        Reference sourceReference = null; // TODO: Determination of sourceReference is redundant
2353
                        IZoologicalName inferredSynName;
2354
                        //for all synonyms of the parent...
2355
                        for (Synonym synonymRelationOfParent:synonyMsOfParent){
2356
                            TaxonName synName;
2357
                            HibernateProxyHelper.deproxy(synonymRelationOfParent);
2358

    
2359
                            synName = synonymRelationOfParent.getName();
2360

    
2361
                            // Set the sourceReference
2362
                            sourceReference = synonymRelationOfParent.getSec();
2363

    
2364
                            // Determine the idInSource
2365
                            String idInSourceParent = getIdInSource(synonymRelationOfParent);
2366

    
2367
                            IZoologicalName parentSynZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2368
                            String synParentGenus = parentSynZooName.getGenusOrUninomial();
2369
                            String synParentInfragenericName = null;
2370
                            String synParentSpecificEpithet = null;
2371

    
2372
                            if (parentSynZooName.isInfraGeneric()){
2373
                                synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2374
                            }
2375
                            if (parentSynZooName.isSpecies()){
2376
                                synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2377
                            }
2378

    
2379
                           /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2380
                                synonymsGenus.put(synGenusName, idInSource);
2381
                            }*/
2382

    
2383
                            //for all synonyms of the taxon
2384

    
2385
                            for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2386

    
2387
                                IZoologicalName zooSynName = getZoologicalName(synonymRelationOfTaxon.getName().getUuid(), zooHashMap);
2388
                                potentialCombination = createPotentialCombination(idInSourceParent, parentSynZooName, zooSynName,
2389
                                        synParentGenus,
2390
                                        synParentInfragenericName,
2391
                                        synParentSpecificEpithet, synonymRelationOfTaxon, zooHashMap);
2392

    
2393
                                taxon.addSynonym(potentialCombination, SynonymType.POTENTIAL_COMBINATION_OF());
2394
                                inferredSynonyms.add(potentialCombination);
2395
                                zooHashMap.put(potentialCombination.getName().getUuid(), potentialCombination.getName());
2396
                                 taxonNames.add(potentialCombination.getName().getNameCache());
2397

    
2398
                            }
2399

    
2400
                        }
2401

    
2402
                        if (doWithMisappliedNames){
2403

    
2404
                            for (TaxonRelationship parentRelationship: taxonRelListParent){
2405

    
2406
                                TaxonName misappliedParentName;
2407

    
2408
                                Taxon misappliedParent = parentRelationship.getFromTaxon();
2409
                                misappliedParentName = misappliedParent.getName();
2410

    
2411
                                HibernateProxyHelper.deproxy(misappliedParent);
2412

    
2413
                                // Set the sourceReference
2414
                                sourceReference = misappliedParent.getSec();
2415

    
2416
                                // Determine the idInSource
2417
                                String idInSourceParent = getIdInSource(misappliedParent);
2418

    
2419
                                IZoologicalName parentSynZooName = getZoologicalName(misappliedParentName.getUuid(), zooHashMap);
2420
                                String synParentGenus = parentSynZooName.getGenusOrUninomial();
2421
                                String synParentInfragenericName = null;
2422
                                String synParentSpecificEpithet = null;
2423

    
2424
                                if (parentSynZooName.isInfraGeneric()){
2425
                                    synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2426
                                }
2427
                                if (parentSynZooName.isSpecies()){
2428
                                    synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2429
                                }
2430

    
2431

    
2432
                                for (TaxonRelationship taxonRelationship: taxonRelListTaxon){
2433
                                    Taxon misappliedName = taxonRelationship.getFromTaxon();
2434
                                    IZoologicalName zooMisappliedName = getZoologicalName(misappliedName.getName().getUuid(), zooHashMap);
2435
                                    potentialCombination = createPotentialCombination(
2436
                                            idInSourceParent, parentSynZooName, zooMisappliedName,
2437
                                            synParentGenus,
2438
                                            synParentInfragenericName,
2439
                                            synParentSpecificEpithet, misappliedName, zooHashMap);
2440

    
2441

    
2442
                                    taxon.addSynonym(potentialCombination, SynonymType.POTENTIAL_COMBINATION_OF());
2443
                                    inferredSynonyms.add(potentialCombination);
2444
                                    zooHashMap.put(potentialCombination.getName().getUuid(), potentialCombination.getName());
2445
                                     taxonNames.add(potentialCombination.getName().getNameCache());
2446
                                }
2447
                            }
2448
                        }
2449

    
2450
                        if (!taxonNames.isEmpty()){
2451
                            List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2452
                            IZoologicalName name;
2453
                            if (!synNotInCDM.isEmpty()){
2454
                                inferredSynonymsToBeRemoved.clear();
2455
                                for (Synonym syn :inferredSynonyms){
2456
                                    try{
2457
                                        name = syn.getName();
2458
                                    }catch (ClassCastException e){
2459
                                        name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2460
                                    }
2461
                                    if (!synNotInCDM.contains(name.getNameCache())){
2462
                                        inferredSynonymsToBeRemoved.add(syn);
2463
                                    }
2464
                                 }
2465
                                // Remove identified Synonyms from inferredSynonyms
2466
                                for (Synonym synonym : inferredSynonymsToBeRemoved) {
2467
                                    inferredSynonyms.remove(synonym);
2468
                                }
2469
                            }
2470
                         }
2471
                        }
2472
                    }else {
2473
                        logger.info("The synonym type is not defined.");
2474
                        return inferredSynonyms;
2475
                    }
2476
                }
2477
            }
2478

    
2479
        }
2480

    
2481
        return inferredSynonyms;
2482
    }
2483

    
2484
    private Synonym createPotentialCombination(String idInSourceParent,
2485
            IZoologicalName parentSynZooName, 	IZoologicalName zooSynName, String synParentGenus,
2486
            String synParentInfragenericName, String synParentSpecificEpithet,
2487
            TaxonBase<?> syn, Map<UUID, IZoologicalName> zooHashMap) {
2488
        Synonym potentialCombination;
2489
        Reference sourceReference;
2490
        IZoologicalName inferredSynName;
2491
        HibernateProxyHelper.deproxy(syn);
2492

    
2493
        // Set sourceReference
2494
        sourceReference = syn.getSec();
2495
        if (sourceReference == null){
2496
            logger.warn("The synonym has no sec reference because it is a misapplied name! Take the sec reference of taxon");
2497
            //TODO:Remove
2498
            if (!parentSynZooName.getTaxa().isEmpty()){
2499
                TaxonBase<?> taxon = parentSynZooName.getTaxa().iterator().next();
2500

    
2501
                sourceReference = taxon.getSec();
2502
            }
2503
        }
2504
        String synTaxonSpecificEpithet = zooSynName.getSpecificEpithet();
2505

    
2506
        String synTaxonInfraSpecificName= null;
2507

    
2508
        if (parentSynZooName.isSpecies()){
2509
            synTaxonInfraSpecificName = zooSynName.getInfraSpecificEpithet();
2510
        }
2511

    
2512
        /*if (epithetName != null && !synonymsEpithet.contains(epithetName)){
2513
            synonymsEpithet.add(epithetName);
2514
        }*/
2515

    
2516
        //create potential combinations...
2517
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(syn.getName().getRank());
2518

    
2519
        inferredSynName.setGenusOrUninomial(synParentGenus);
2520
        if (zooSynName.isSpecies()){
2521
              inferredSynName.setSpecificEpithet(synTaxonSpecificEpithet);
2522
              if (parentSynZooName.isInfraGeneric()){
2523
                  inferredSynName.setInfraGenericEpithet(synParentInfragenericName);
2524
              }
2525
        }
2526
        if (zooSynName.isInfraSpecific()){
2527
            inferredSynName.setSpecificEpithet(synParentSpecificEpithet);
2528
            inferredSynName.setInfraSpecificEpithet(synTaxonInfraSpecificName);
2529
        }
2530
        if (parentSynZooName.isInfraGeneric()){
2531
            inferredSynName.setInfraGenericEpithet(synParentInfragenericName);
2532
        }
2533

    
2534

    
2535
        potentialCombination = Synonym.NewInstance(inferredSynName, null);
2536

    
2537
        // Set the sourceReference
2538
        potentialCombination.setSec(sourceReference);
2539

    
2540

    
2541
        // Determine the idInSource
2542
        String idInSourceSyn= getIdInSource(syn);
2543

    
2544
        if (idInSourceParent != null && idInSourceSyn != null) {
2545
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2546
            inferredSynName.addSource(originalSource);
2547
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2548
            potentialCombination.addSource(originalSource);
2549
        }
2550

    
2551
        return potentialCombination;
2552
    }
2553

    
2554
    private Synonym createInferredGenus(Taxon taxon,
2555
            Map<UUID, IZoologicalName> zooHashMap, IZoologicalName taxonName,
2556
            String epithetOfTaxon, String genusOfTaxon,
2557
            List<String> taxonNames, IZoologicalName zooParentName,
2558
            TaxonBase syn) {
2559

    
2560
        Synonym inferredGenus;
2561
        TaxonName synName;
2562
        IZoologicalName inferredSynName;
2563
        synName =syn.getName();
2564
        HibernateProxyHelper.deproxy(syn);
2565

    
2566
        // Determine the idInSource
2567
        String idInSourceSyn = getIdInSource(syn);
2568
        String idInSourceTaxon = getIdInSource(taxon);
2569
        // Determine the sourceReference
2570
        Reference sourceReference = syn.getSec();
2571

    
2572
        //logger.warn(sourceReference.getTitleCache());
2573

    
2574
        synName = syn.getName();
2575
        IZoologicalName synZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2576
        String synSpeciesEpithetName = synZooName.getSpecificEpithet();
2577
                     /* if (synonymsEpithet != null && !synonymsEpithet.contains(synSpeciesEpithetName)){
2578
            synonymsEpithet.add(synSpeciesEpithetName);
2579
        }*/
2580

    
2581
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2582
        //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...
2583

    
2584

    
2585
        inferredSynName.setGenusOrUninomial(genusOfTaxon);
2586
        if (zooParentName.isInfraGeneric()){
2587
            inferredSynName.setInfraGenericEpithet(zooParentName.getInfraGenericEpithet());
2588
        }
2589

    
2590
        if (taxonName.isSpecies()){
2591
            inferredSynName.setSpecificEpithet(synSpeciesEpithetName);
2592
        }
2593
        if (taxonName.isInfraSpecific()){
2594
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2595
            inferredSynName.setInfraSpecificEpithet(synZooName.getInfraGenericEpithet());
2596
        }
2597

    
2598

    
2599
        inferredGenus = Synonym.NewInstance(inferredSynName, null);
2600

    
2601
        // Set the sourceReference
2602
        inferredGenus.setSec(sourceReference);
2603

    
2604
        // Add the original source
2605
        if (idInSourceSyn != null && idInSourceTaxon != null) {
2606
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2607
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2608
            inferredGenus.addSource(originalSource);
2609

    
2610
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2611
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2612
            inferredSynName.addSource(originalSource);
2613
            originalSource = null;
2614

    
2615
        }else{
2616
            logger.error("There is an idInSource missing: " + idInSourceSyn + " of Synonym or " + idInSourceTaxon + " of Taxon");
2617
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2618
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2619
            inferredGenus.addSource(originalSource);
2620

    
2621
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2622
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2623
            inferredSynName.addSource(originalSource);
2624
            originalSource = null;
2625
        }
2626

    
2627
        taxon.addSynonym(inferredGenus, SynonymType.INFERRED_GENUS_OF());
2628

    
2629
        return inferredGenus;
2630
    }
2631

    
2632
    private Synonym createInferredEpithets(Taxon taxon,
2633
            Map<UUID, IZoologicalName> zooHashMap, IZoologicalName taxonName,
2634
            String epithetOfTaxon, String infragenericEpithetOfTaxon,
2635
            String infraspecificEpithetOfTaxon, List<String> taxonNames,
2636
            TaxonName parentName, TaxonBase<?> syn) {
2637

    
2638
        Synonym inferredEpithet;
2639
        TaxonName synName;
2640
        IZoologicalName inferredSynName;
2641
        HibernateProxyHelper.deproxy(syn);
2642

    
2643
        // Determine the idInSource
2644
        String idInSourceSyn = getIdInSource(syn);
2645
        String idInSourceTaxon =  getIdInSource(taxon);
2646
        // Determine the sourceReference
2647
        Reference sourceReference = syn.getSec();
2648

    
2649
        if (sourceReference == null){
2650
             logger.warn("The synonym has no sec reference because it is a misapplied name! Take the sec reference of taxon" + taxon.getSec());
2651
             sourceReference = taxon.getSec();
2652
        }
2653

    
2654
        synName = syn.getName();
2655
        IZoologicalName zooSynName = getZoologicalName(synName.getUuid(), zooHashMap);
2656
        String synGenusName = zooSynName.getGenusOrUninomial();
2657
        String synInfraGenericEpithet = null;
2658
        String synSpecificEpithet = null;
2659

    
2660
        if (zooSynName.getInfraGenericEpithet() != null){
2661
            synInfraGenericEpithet = zooSynName.getInfraGenericEpithet();
2662
        }
2663

    
2664
        if (zooSynName.isInfraSpecific()){
2665
            synSpecificEpithet = zooSynName.getSpecificEpithet();
2666
        }
2667

    
2668
                     /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2669
            synonymsGenus.put(synGenusName, idInSource);
2670
        }*/
2671

    
2672
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2673

    
2674
        // DEBUG TODO: for subgenus or subspecies the infrageneric or infraspecific epithet should be used!!!
2675
        if (epithetOfTaxon == null && infragenericEpithetOfTaxon == null && infraspecificEpithetOfTaxon == null) {
2676
            logger.error("This specificEpithet is NULL" + taxon.getTitleCache());
2677
        }
2678
        inferredSynName.setGenusOrUninomial(synGenusName);
2679

    
2680
        if (parentName.isInfraGeneric()){
2681
            inferredSynName.setInfraGenericEpithet(synInfraGenericEpithet);
2682
        }
2683
        if (taxonName.isSpecies()){
2684
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2685
        }else if (taxonName.isInfraSpecific()){
2686
            inferredSynName.setSpecificEpithet(synSpecificEpithet);
2687
            inferredSynName.setInfraSpecificEpithet(infraspecificEpithetOfTaxon);
2688
        }
2689

    
2690
        inferredEpithet = Synonym.NewInstance(inferredSynName, null);
2691

    
2692
        // Set the sourceReference
2693
        inferredEpithet.setSec(sourceReference);
2694

    
2695
        /* Add the original source
2696
        if (idInSource != null) {
2697
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(idInSource, "InferredEpithetOf", syn.getSec(), null);
2698

    
2699
            // Add the citation
2700
            Reference citation = getCitation(syn);
2701
            if (citation != null) {
2702
                originalSource.setCitation(citation);
2703
                inferredEpithet.addSource(originalSource);
2704
            }
2705
        }*/
2706
        String taxonId = idInSourceTaxon+ "; " + idInSourceSyn;
2707

    
2708

    
2709
        IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2710
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2711

    
2712
        inferredEpithet.addSource(originalSource);
2713

    
2714
        originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2715
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2716

    
2717
        inferredSynName.addSource(originalSource);
2718

    
2719

    
2720

    
2721
        taxon.addSynonym(inferredEpithet, SynonymType.INFERRED_EPITHET_OF());
2722

    
2723
        return inferredEpithet;
2724
    }
2725

    
2726
    /**
2727
     * Returns an existing IZoologicalName or extends an internal hashmap if it does not exist.
2728
     * Very likely only useful for createInferredSynonyms().
2729
     * @param uuid
2730
     * @param zooHashMap
2731
     * @return
2732
     */
2733
    private IZoologicalName getZoologicalName(UUID uuid, Map <UUID, IZoologicalName> zooHashMap) {
2734
        IZoologicalName taxonName =nameDao.findZoologicalNameByUUID(uuid);
2735
        if (taxonName == null) {
2736
            taxonName = zooHashMap.get(uuid);
2737
        }
2738
        return taxonName;
2739
    }
2740

    
2741
    /**
2742
     * Returns the idInSource for a given Synonym.
2743
     * @param syn
2744
     */
2745
    private String getIdInSource(TaxonBase<?> taxonBase) {
2746
        String idInSource = null;
2747
        Set<IdentifiableSource> sources = taxonBase.getSources();
2748
        if (sources.size() == 1) {
2749
            IdentifiableSource source = sources.iterator().next();
2750
            if (source != null) {
2751
                idInSource  = source.getIdInSource();
2752
            }
2753
        } else if (sources.size() > 1) {
2754
            int count = 1;
2755
            idInSource = "";
2756
            for (IdentifiableSource source : sources) {
2757
                idInSource += source.getIdInSource();
2758
                if (count < sources.size()) {
2759
                    idInSource += "; ";
2760
                }
2761
                count++;
2762
            }
2763
        } else if (sources.size() == 0){
2764
            logger.warn("No idInSource for TaxonBase " + taxonBase.getUuid() + " - " + taxonBase.getTitleCache());
2765
        }
2766

    
2767

    
2768
        return idInSource;
2769
    }
2770

    
2771

    
2772
    /**
2773
     * Returns the citation for a given Synonym.
2774
     * @param syn
2775
     */
2776
    private Reference getCitation(Synonym syn) {
2777
        Reference citation = null;
2778
        Set<IdentifiableSource> sources = syn.getSources();
2779
        if (sources.size() == 1) {
2780
            IdentifiableSource source = sources.iterator().next();
2781
            if (source != null) {
2782
                citation = source.getCitation();
2783
            }
2784
        } else if (sources.size() > 1) {
2785
            logger.warn("This Synonym has more than one source: " + syn.getUuid() + " (" + syn.getTitleCache() +")");
2786
        }
2787

    
2788
        return citation;
2789
    }
2790

    
2791
    @Override
2792
    public List<Synonym>  createAllInferredSynonyms(Taxon taxon, Classification tree, boolean doWithMisappliedNames){
2793
        List <Synonym> inferredSynonyms = new ArrayList<>();
2794

    
2795
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.INFERRED_EPITHET_OF(), doWithMisappliedNames));
2796
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.INFERRED_GENUS_OF(), doWithMisappliedNames));
2797
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.POTENTIAL_COMBINATION_OF(), doWithMisappliedNames));
2798

    
2799
        return inferredSynonyms;
2800
    }
2801

    
2802
    @Override
2803
    public List<Classification> listClassifications(TaxonBase taxonBase, Integer limit, Integer start, List<String> propertyPaths) {
2804

    
2805
        // TODO quickly implemented, create according dao !!!!
2806
        Set<TaxonNode> nodes = new HashSet<>();
2807
        Set<Classification> classifications = new HashSet<>();
2808
        List<Classification> list = new ArrayList<>();
2809

    
2810
        if (taxonBase == null) {
2811
            return list;
2812
        }
2813

    
2814
        taxonBase = load(taxonBase.getUuid());
2815

    
2816
        if (taxonBase instanceof Taxon) {
2817
            nodes.addAll(((Taxon)taxonBase).getTaxonNodes());
2818
        } else {
2819
            Taxon taxon = ((Synonym)taxonBase).getAcceptedTaxon();
2820
            if (taxon != null){
2821
                nodes.addAll(taxon.getTaxonNodes());
2822
            }
2823
        }
2824
        for (TaxonNode node : nodes) {
2825
            classifications.add(node.getClassification());
2826
        }
2827
        list.addAll(classifications);
2828
        return list;
2829
    }
2830

    
2831
    @Override
2832
    @Transactional(readOnly = false)
2833
    public UpdateResult changeRelatedTaxonToSynonym(UUID fromTaxonUuid,
2834
            UUID toTaxonUuid,
2835
            TaxonRelationshipType oldRelationshipType,
2836
            SynonymType synonymType) throws DataChangeNoRollbackException {
2837
        UpdateResult result = new UpdateResult();
2838
        Taxon fromTaxon = (Taxon) dao.load(fromTaxonUuid);
2839
        Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
2840
        result = changeRelatedTaxonToSynonym(fromTaxon, toTaxon, oldRelationshipType, synonymType);
2841

    
2842
        result.addUpdatedObject(fromTaxon);
2843
        result.addUpdatedObject(toTaxon);
2844
        result.addUpdatedObject(result.getCdmEntity());
2845

    
2846
        return result;
2847
    }
2848

    
2849
    @Override
2850
    @Transactional(readOnly = false)
2851
    public UpdateResult changeRelatedTaxonToSynonym(Taxon fromTaxon, Taxon toTaxon, TaxonRelationshipType oldRelationshipType,
2852
            SynonymType synonymType) throws DataChangeNoRollbackException {
2853

    
2854
        UpdateResult result = new UpdateResult();
2855
        // Create new synonym using concept name
2856
        TaxonName synonymName = fromTaxon.getName();
2857

    
2858
        // Remove concept relation from taxon
2859
        toTaxon.removeTaxon(fromTaxon, oldRelationshipType);
2860

    
2861
        // Create a new synonym for the taxon
2862
        Synonym synonym;
2863
        if (synonymType != null
2864
                && synonymType.equals(SynonymType.HOMOTYPIC_SYNONYM_OF())){
2865
            synonym = Synonym.NewInstance(synonymName, fromTaxon.getSec());
2866
            toTaxon.addHomotypicSynonym(synonym);
2867
        } else{
2868
            synonym = toTaxon.addHeterotypicSynonymName(synonymName);
2869
        }
2870

    
2871
        this.saveOrUpdate(toTaxon);
2872
        //TODO: configurator and classification
2873
        TaxonDeletionConfigurator config = new TaxonDeletionConfigurator();
2874
        config.setDeleteNameIfPossible(false);
2875
        result.includeResult(this.deleteTaxon(fromTaxon.getUuid(), config, null));
2876
        result.setCdmEntity(synonym);
2877
        result.addUpdatedObject(toTaxon);
2878
        result.addUpdatedObject(synonym);
2879
        return result;
2880
    }
2881

    
2882
    @Override
2883
    public DeleteResult isDeletable(UUID taxonBaseUuid, DeleteConfiguratorBase config){
2884
        DeleteResult result = new DeleteResult();
2885
        TaxonBase<?> taxonBase = load(taxonBaseUuid);
2886
        Set<CdmBase> references = commonService.getReferencingObjectsForDeletion(taxonBase);
2887
        if (taxonBase instanceof Taxon){
2888
            TaxonDeletionConfigurator taxonConfig = (TaxonDeletionConfigurator) config;
2889
            result = isDeletableForTaxon(references, taxonConfig);
2890
        }else{
2891
            SynonymDeletionConfigurator synonymConfig = (SynonymDeletionConfigurator) config;
2892
            result = isDeletableForSynonym(references, synonymConfig);
2893
        }
2894
        return result;
2895
    }
2896

    
2897
    private DeleteResult isDeletableForSynonym(Set<CdmBase> references, SynonymDeletionConfigurator config){
2898
        String message;
2899
        DeleteResult result = new DeleteResult();
2900
        for (CdmBase ref: references){
2901
            if (!(ref instanceof Taxon || ref instanceof TaxonName )){
2902
                message = "The Synonym can't be deleted as long as it is referenced by " + ref.getClass().getSimpleName() + " with id "+ ref.getId();
2903
                result.addException(new ReferencedObjectUndeletableException(message));
2904
                result.addRelatedObject(ref);
2905
                result.setAbort();
2906
            }
2907
        }
2908

    
2909
        return result;
2910
    }
2911

    
2912
    private DeleteResult isDeletableForTaxon(Set<CdmBase> references, TaxonDeletionConfigurator config){
2913
        String message = null;
2914
        DeleteResult result = new DeleteResult();
2915
        for (CdmBase ref: references){
2916
            if (!(ref instanceof TaxonName)){
2917
            	message = null;
2918
                if (!config.isDeleteSynonymRelations() && (ref instanceof Synonym)){
2919
                    message = "The taxon can't be deleted as long as it has synonyms.";
2920
                }
2921
                if (!config.isDeleteDescriptions() && (ref instanceof DescriptionBase)){
2922
                    message = "The taxon can't be deleted as long as it has factual data.";
2923
                }
2924

    
2925
                if (!config.isDeleteTaxonNodes() && (ref instanceof TaxonNode)){
2926
                    message = "The taxon can't be deleted as long as it belongs to a taxon node.";
2927
                }
2928
                if (!config.isDeleteTaxonRelationships() && (ref instanceof TaxonRelationship)){
2929
                    if (!config.isDeleteMisappliedNamesAndInvalidDesignations() &&
2930
                            (((TaxonRelationship)ref).getType().isMisappliedNameOrInvalidDesignation())){
2931
                        message = "The taxon can't be deleted as long as it has misapplied names or invalid designations.";
2932
                    } else{
2933
                        message = "The taxon can't be deleted as long as it belongs to taxon relationship.";
2934
                    }
2935
                }
2936
                if (ref instanceof PolytomousKeyNode){
2937
                    message = "The taxon can't be deleted as long as it is referenced by a polytomous key node.";
2938
                }
2939

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

    
2944

    
2945
               /* //PolytomousKeyNode
2946
                if (referencingObject.isInstanceOf(PolytomousKeyNode.class)){
2947
                    String message = "Taxon" + taxon.getTitleCache() + " can't be deleted as it is used in polytomous key node";
2948
                    return message;
2949
                }*/
2950

    
2951
                //TaxonInteraction
2952
                if (ref.isInstanceOf(TaxonInteraction.class)){
2953
                    message = "Taxon can't be deleted as it is used in taxonInteraction#taxon2";
2954
                }
2955

    
2956
              //TaxonInteraction
2957
                if (ref.isInstanceOf(DeterminationEvent.class)){
2958
                    message = "Taxon can't be deleted as it is used in a determination event";
2959
                }
2960
            }
2961
            if (message != null){
2962
	            result.addException(new ReferencedObjectUndeletableException(message));
2963
	            result.addRelatedObject(ref);
2964
	            result.setAbort();
2965
            }
2966
        }
2967

    
2968
        return result;
2969
    }
2970

    
2971
    @Override
2972
    public IncludedTaxaDTO listIncludedTaxa(UUID taxonUuid, IncludedTaxonConfiguration config) {
2973
        IncludedTaxaDTO result = new IncludedTaxaDTO(taxonUuid);
2974

    
2975
        //preliminary implementation
2976

    
2977
        Set<Taxon> taxa = new HashSet<>();
2978
        TaxonBase<?> taxonBase = find(taxonUuid);
2979
        if (taxonBase == null){
2980
            return new IncludedTaxaDTO();
2981
        }else if (taxonBase.isInstanceOf(Taxon.class)){
2982
            Taxon taxon = CdmBase.deproxy(taxonBase, Taxon.class);
2983
            taxa.add(taxon);
2984
        }else if (taxonBase.isInstanceOf(Synonym.class)){
2985
            //TODO partial synonyms ??
2986
            //TODO synonyms in general
2987
            Synonym syn = CdmBase.deproxy(taxonBase, Synonym.class);
2988
            taxa.add(syn.getAcceptedTaxon());
2989
        }else{
2990
            throw new IllegalArgumentException("Unhandled class " + taxonBase.getClass().getSimpleName());
2991
        }
2992

    
2993
        Set<Taxon> related = makeRelatedIncluded(taxa, result, config);
2994
        int i = 0;
2995
        while((! related.isEmpty()) && i++ < 100){  //to avoid
2996
             related = makeRelatedIncluded(related, result, config);
2997
        }
2998

    
2999
        return result;
3000
    }
3001

    
3002
    /**
3003
     * Computes all children and conceptually congruent and included taxa and adds them to the existingTaxa
3004
     * data structure.
3005
     * @return the set of conceptually related taxa for further use
3006
     */
3007
    /**
3008
     * @param uncheckedTaxa
3009
     * @param existingTaxa
3010
     * @param config
3011
     * @return
3012
     */
3013
    private Set<Taxon> makeRelatedIncluded(Set<Taxon> uncheckedTaxa, IncludedTaxaDTO existingTaxa, IncludedTaxonConfiguration config) {
3014

    
3015
        //children
3016
        Set<TaxonNode> taxonNodes = new HashSet<>();
3017
        for (Taxon taxon: uncheckedTaxa){
3018
            taxonNodes.addAll(taxon.getTaxonNodes());
3019
        }
3020

    
3021
        Set<Taxon> children = new HashSet<>();
3022
        if (! config.onlyCongruent){
3023
            for (TaxonNode node: taxonNodes){
3024
                List<TaxonNode> childNodes = nodeService.loadChildNodesOfTaxonNode(node, null, true, null);
3025
                for (TaxonNode child : childNodes){
3026
                    children.add(child.getTaxon());
3027
                }
3028
            }
3029
            children.remove(null);  // just to be on the save side
3030
        }
3031

    
3032
        Iterator<Taxon> it = children.iterator();
3033
        while(it.hasNext()){
3034
            UUID uuid = it.next().getUuid();
3035
            if (existingTaxa.contains(uuid)){
3036
                it.remove();
3037
            }else{
3038
                existingTaxa.addIncludedTaxon(uuid, new ArrayList<>(), false);
3039
            }
3040
        }
3041

    
3042
        //concept relations
3043
        Set<Taxon> uncheckedAndChildren = new HashSet<>(uncheckedTaxa);
3044
        uncheckedAndChildren.addAll(children);
3045

    
3046
        Set<Taxon> relatedTaxa = makeConceptIncludedTaxa(uncheckedAndChildren, existingTaxa, config);
3047

    
3048

    
3049
        Set<Taxon> result = new HashSet<>(relatedTaxa);
3050
        return result;
3051
    }
3052

    
3053
    /**
3054
     * Computes all conceptually congruent or included taxa and adds them to the existingTaxa data structure.
3055
     * @return the set of these computed taxa
3056
     */
3057
    private Set<Taxon> makeConceptIncludedTaxa(Set<Taxon> unchecked, IncludedTaxaDTO existingTaxa, IncludedTaxonConfiguration config) {
3058
        Set<Taxon> result = new HashSet<>();
3059

    
3060
        for (Taxon taxon : unchecked){
3061
            Set<TaxonRelationship> fromRelations = taxon.getRelationsFromThisTaxon();
3062
            Set<TaxonRelationship> toRelations = taxon.getRelationsToThisTaxon();
3063

    
3064
            for (TaxonRelationship fromRel : fromRelations){
3065
                if (config.includeDoubtful == false && fromRel.isDoubtful()){
3066
                    continue;
3067
                }
3068
                if (fromRel.getType().equals(TaxonRelationshipType.CONGRUENT_TO()) ||
3069
                        !config.onlyCongruent && fromRel.getType().equals(TaxonRelationshipType.INCLUDES()) ||
3070
                        !config.onlyCongruent && fromRel.getType().equals(TaxonRelationshipType.CONGRUENT_OR_INCLUDES())
3071
                        ){
3072
                    result.add(fromRel.getToTaxon());
3073
                }
3074
            }
3075

    
3076
            for (TaxonRelationship toRel : toRelations){
3077
                if (config.includeDoubtful == false && toRel.isDoubtful()){
3078
                    continue;
3079
                }
3080
                if (toRel.getType().equals(TaxonRelationshipType.CONGRUENT_TO())){
3081
                    result.add(toRel.getFromTaxon());
3082
                }
3083
            }
3084
        }
3085

    
3086
        Iterator<Taxon> it = result.iterator();
3087
        while(it.hasNext()){
3088
            UUID uuid = it.next().getUuid();
3089
            if (existingTaxa.contains(uuid)){
3090
                it.remove();
3091
            }else{
3092
                existingTaxa.addIncludedTaxon(uuid, new ArrayList<>(), false);
3093
            }
3094
        }
3095
        return result;
3096
    }
3097

    
3098
    @Override
3099
    public List<TaxonBase> findTaxaByName(MatchingTaxonConfigurator config){
3100
        List<TaxonBase> taxonList = dao.getTaxaByName(true, config.isIncludeSynonyms(), false, false, false,
3101
                config.getTaxonNameTitle(), null, MatchMode.EXACT, null, null, 0, 0, config.getPropertyPath());
3102
        return taxonList;
3103
    }
3104

    
3105
	@Override
3106
	@Transactional(readOnly = true)
3107
	public <S extends TaxonBase> Pager<IdentifiedEntityDTO<S>> findByIdentifier(
3108
			Class<S> clazz, String identifier, DefinedTerm identifierType, TaxonNode subtreeFilter,
3109
			MatchMode matchmode, boolean includeEntity, Integer pageSize,
3110
			Integer pageNumber,	List<String> propertyPaths) {
3111
		if (subtreeFilter == null){
3112
			return findByIdentifier(clazz, identifier, identifierType, matchmode, includeEntity, pageSize, pageNumber, propertyPaths);
3113
		}
3114

    
3115
		Integer numberOfResults = dao.countByIdentifier(clazz, identifier, identifierType, subtreeFilter, matchmode);
3116
        List<Object[]> daoResults = new ArrayList<>();
3117
        if(numberOfResults > 0) { // no point checking again
3118
        	daoResults = dao.findByIdentifier(clazz, identifier, identifierType, subtreeFilter,
3119
    				matchmode, includeEntity, pageSize, pageNumber, propertyPaths);
3120
        }
3121

    
3122
        List<IdentifiedEntityDTO<S>> result = new ArrayList<>();
3123
        for (Object[] daoObj : daoResults){
3124
        	if (includeEntity){
3125
        		result.add(new IdentifiedEntityDTO<S>((DefinedTerm)daoObj[0], (String)daoObj[1], (S)daoObj[2]));
3126
        	}else{
3127
        		result.add(new IdentifiedEntityDTO<S>((DefinedTerm)daoObj[0], (String)daoObj[1], (UUID)daoObj[2], (String)daoObj[3], null));
3128
        	}
3129
        }
3130
		return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, result);
3131
	}
3132

    
3133
	@Override
3134
    @Transactional(readOnly = true)
3135
    public <S extends TaxonBase> Pager<MarkedEntityDTO<S>> findByMarker(
3136
            Class<S> clazz, MarkerType markerType, Boolean markerValue,
3137
            TaxonNode subtreeFilter, boolean includeEntity, TaxonTitleType titleType,
3138
            Integer pageSize, Integer pageNumber, List<String> propertyPaths) {
3139
        if (subtreeFilter == null){
3140
            return super.findByMarker (clazz, markerType, markerValue, includeEntity, pageSize, pageNumber, propertyPaths);
3141
        }
3142

    
3143
        Long numberOfResults = dao.countByMarker(clazz, markerType, markerValue, subtreeFilter);
3144
        List<Object[]> daoResults = new ArrayList<>();
3145
        if(numberOfResults > 0) { // no point checking again
3146
            daoResults = dao.findByMarker(clazz, markerType, markerValue, subtreeFilter,
3147
                    includeEntity, titleType, pageSize, pageNumber, propertyPaths);
3148
        }
3149

    
3150
        List<MarkedEntityDTO<S>> result = new ArrayList<>();
3151
        for (Object[] daoObj : daoResults){
3152
            if (includeEntity){
3153
                result.add(new MarkedEntityDTO<S>((MarkerType)daoObj[0], (Boolean)daoObj[1], (S)daoObj[2]));
3154
            }else{
3155
                result.add(new MarkedEntityDTO<S>((MarkerType)daoObj[0], (Boolean)daoObj[1], (UUID)daoObj[2], (String)daoObj[3]));
3156
            }
3157
        }
3158
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, result);
3159
    }
3160

    
3161
    @Override
3162
	@Transactional(readOnly = false)
3163
	public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym, UUID newTaxonUUID, boolean moveHomotypicGroup,
3164
            SynonymType newSynonymType, Reference newSecundum, String newSecundumDetail,
3165
            boolean keepSecundumIfUndefined) throws HomotypicalGroupChangeException {
3166

    
3167
	    UpdateResult result = new UpdateResult();
3168
		Taxon newTaxon = CdmBase.deproxy(dao.load(newTaxonUUID),Taxon.class);
3169
		result = moveSynonymToAnotherTaxon(oldSynonym, newTaxon, moveHomotypicGroup, newSynonymType,
3170
		        newSecundum, newSecundumDetail, keepSecundumIfUndefined);
3171

    
3172
		return result;
3173
	}
3174

    
3175
	@Override
3176
	public UpdateResult moveFactualDateToAnotherTaxon(UUID fromTaxonUuid, UUID toTaxonUuid){
3177
		UpdateResult result = new UpdateResult();
3178

    
3179
		Taxon fromTaxon = (Taxon)dao.load(fromTaxonUuid);
3180
		Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
3181
    	for(TaxonDescription description : fromTaxon.getDescriptions()){
3182
              //reload to avoid session conflicts
3183
              description = HibernateProxyHelper.deproxy(description, TaxonDescription.class);
3184

    
3185
              String moveMessage = String.format("Description moved from %s", fromTaxon);
3186
              if(description.isProtectedTitleCache()){
3187
                  String separator = "";
3188
                  if(!StringUtils.isBlank(description.getTitleCache())){
3189
                      separator = " - ";
3190
                  }
3191
                  description.setTitleCache(description.getTitleCache() + separator + moveMessage, true);
3192
              }
3193
              Annotation annotation = Annotation.NewInstance(moveMessage, Language.getDefaultLanguage());
3194
              annotation.setAnnotationType(AnnotationType.TECHNICAL());
3195
              description.addAnnotation(annotation);
3196
              toTaxon.addDescription(description);
3197
              dao.saveOrUpdate(toTaxon);
3198
              dao.saveOrUpdate(fromTaxon);
3199
              result.addUpdatedObject(toTaxon);
3200
              result.addUpdatedObject(fromTaxon);
3201

    
3202
        }
3203

    
3204
    	return result;
3205
	}
3206

    
3207
	@Override
3208
	@Transactional(readOnly = false)
3209
	public UpdateResult swapSynonymAndAcceptedTaxon(UUID synonymUUid,
3210
			UUID acceptedTaxonUuid) {
3211
		TaxonBase<?> base = this.load(synonymUUid);
3212
		Synonym syn = HibernateProxyHelper.deproxy(base, Synonym.class);
3213
		base = this.load(acceptedTaxonUuid);
3214
		Taxon taxon = HibernateProxyHelper.deproxy(base, Taxon.class);
3215

    
3216
		return this.swapSynonymAndAcceptedTaxon(syn, taxon);
3217
	}
3218

    
3219

    
3220
}
(99-99/105)