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
    @Override
296
    @Transactional(readOnly = false)
297
    public UpdateResult changeSynonymToRelatedTaxon(UUID synonymUuid,
298
            UUID toTaxonUuid,
299
            TaxonRelationshipType taxonRelationshipType,
300
            Reference citation,
301
            String microcitation){
302

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

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

    
322
        UpdateResult result = new UpdateResult();
323

    
324
        TaxonName synonymName = synonym.getName();
325

    
326
      /*  // remove synonym from taxon
327
        toTaxon.removeSynonym(synonym);
328
*/
329
        // Create a taxon with synonym name
330
        Taxon fromTaxon = Taxon.NewInstance(synonymName, null);
331
        fromTaxon.setAppendedPhrase(synonym.getAppendedPhrase());
332

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

    
341
        return result;
342
    }
343

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

    
352
        // Switch groups
353
        oldHomotypicalGroup.removeTypifiedName(synonymName, false);
354
        newHomotypicalGroup.addTypifiedName(synonymName);
355

    
356
        //remove existing basionym relationships
357
        synonymName.removeBasionyms();
358

    
359
        //add basionym relationship
360
        if (setBasionymRelationIfApplicable){
361
            Set<TaxonName> basionyms = newHomotypicalGroup.getBasionyms();
362
            for (TaxonName basionym : basionyms){
363
                synonymName.addBasionym(basionym);
364
            }
365
        }
366

    
367
        //set synonym relationship correctly
368
        Taxon acceptedTaxon = synonym.getAcceptedTaxon();
369

    
370
        boolean hasNewTargetTaxon = targetTaxon != null && !targetTaxon.equals(acceptedTaxon);
371
        if (acceptedTaxon != null){
372

    
373
            HomotypicalGroup acceptedGroup = acceptedTaxon.getHomotypicGroup();
374
            boolean isHomotypicToTaxon = acceptedGroup.equals(newHomotypicalGroup);
375
            SynonymType newRelationType = isHomotypicToTaxon? SynonymType.HOMOTYPIC_SYNONYM_OF() : SynonymType.HETEROTYPIC_SYNONYM_OF();
376
            synonym.setType(newRelationType);
377

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

    
390
    }
391

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

    
401
    @Override
402
    @Autowired
403
    protected void setDao(ITaxonDao dao) {
404
        this.dao = dao;
405
    }
406

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

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

    
419
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
420
    }
421

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

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

    
434
        return results;
435
    }
436

    
437
    @Override
438
    public List<TaxonRelationship> listToTaxonRelationships(Taxon taxon, TaxonRelationshipType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths){
439
        Integer numberOfResults = dao.countTaxonRelationships(taxon, type, TaxonRelationship.Direction.relatedTo);
440

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

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

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

    
459
    @Override
460
    public List<TaxonRelationship> listFromTaxonRelationships(Taxon taxon, TaxonRelationshipType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths){
461
        Integer numberOfResults = dao.countTaxonRelationships(taxon, type, TaxonRelationship.Direction.relatedFrom);
462

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

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

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

    
481
    @Override
482
    public List<TaxonRelationship> listTaxonRelationships(Set<TaxonRelationshipType> types,
483
            Integer pageSize, Integer pageStart, List<OrderHint> orderHints, List<String> propertyPaths) {
484
        Long numberOfResults = dao.countTaxonRelationships(types);
485

    
486
        List<TaxonRelationship> results = new ArrayList<>();
487
        if(numberOfResults > 0) {
488
            results = dao.getTaxonRelationships(types, pageSize, pageStart, orderHints, propertyPaths);
489
        }
490
        return results;
491
    }
492

    
493
    @Override
494
    public Taxon findAcceptedTaxonFor(UUID synonymUuid, UUID classificationUuid, List<String> propertyPaths){
495

    
496
        Taxon result = null;
497
        Long count = 0l;
498

    
499
        Synonym synonym = null;
500

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

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

    
518
            }
519
        }
520

    
521
        count = dao.countAcceptedTaxonFor(synonym, classificationFilter) ;
522
        if(count > 0){
523
            result = dao.acceptedTaxonFor(synonym, classificationFilter, propertyPaths);
524
        }
525

    
526
        return result;
527
    }
528

    
529

    
530
    @Override
531
    public Set<Taxon> listRelatedTaxa(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, Integer maxDepth,
532
            Integer limit, Integer start, List<String> propertyPaths) {
533

    
534
        Set<Taxon> relatedTaxa = collectRelatedTaxa(taxon, includeRelationships, new HashSet<>(), maxDepth);
535
        relatedTaxa.remove(taxon);
536
        beanInitializer.initializeAll(relatedTaxa, propertyPaths);
537
        return relatedTaxa;
538
    }
539

    
540

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

    
553
        if(taxa.isEmpty()) {
554
            taxa.add(taxon);
555
        }
556

    
557
        if(includeRelationships.isEmpty()){
558
            return taxa;
559
        }
560

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

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

    
601
    @Override
602
    public Pager<Synonym> getSynonyms(Taxon taxon,	SynonymType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
603
        Long numberOfResults = dao.countSynonyms(taxon, type);
604

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

    
610
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
611
    }
612

    
613
    @Override
614
    public List<List<Synonym>> getSynonymsByHomotypicGroup(Taxon taxon, List<String> propertyPaths){
615
        List<List<Synonym>> result = new ArrayList<>();
616
        taxon = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
617
        HomotypicGroupTaxonComparator comparator = new HomotypicGroupTaxonComparator(taxon);
618

    
619

    
620
        //homotypic
621
        result.add(taxon.getHomotypicSynonymsByHomotypicGroup(comparator));
622

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

    
629
        return result;
630

    
631
    }
632

    
633
    @Override
634
    public List<Synonym> getHomotypicSynonymsByHomotypicGroup(Taxon taxon, List<String> propertyPaths){
635
        Taxon t = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
636
        HomotypicGroupTaxonComparator comparator = new HomotypicGroupTaxonComparator(taxon);
637

    
638
        return t.getHomotypicSynonymsByHomotypicGroup(comparator);
639
    }
640

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

    
652
    @Override
653
    public List<UuidAndTitleCache<IdentifiableEntity>> findTaxaAndNamesForEditor(IFindTaxaAndNamesConfigurator configurator){
654

    
655
        List<UuidAndTitleCache<IdentifiableEntity>> results = new ArrayList<>();
656

    
657

    
658
        if (configurator.isDoSynonyms() || configurator.isDoTaxa() || configurator.isDoNamesWithoutTaxa() || configurator.isDoTaxaByCommonNames()){
659
        	results = dao.getTaxaByNameForEditor(configurator.isDoTaxa(), configurator.isDoSynonyms(), configurator.isDoNamesWithoutTaxa(), configurator.isDoMisappliedNames(), configurator.isDoTaxaByCommonNames(), configurator.getTitleSearchStringSqlized(), configurator.getClassification(), configurator.getMatchMode(), configurator.getNamedAreas(), configurator.getOrder());
660
        }
661

    
662
        return results;
663
    }
664

    
665
    @Override
666
    public Pager<IdentifiableEntity> findTaxaAndNames(IFindTaxaAndNamesConfigurator configurator) {
667

    
668
        List<IdentifiableEntity> results = new ArrayList<>();
669
        long numberOfResults = 0; // overall number of results (as opposed to number of results per page)
670
        List<TaxonBase> taxa = null;
671

    
672
        // Taxa and synonyms
673
        long numberTaxaResults = 0L;
674

    
675

    
676
        List<String> propertyPath = new ArrayList<>();
677
        if(configurator.getTaxonPropertyPath() != null){
678
            propertyPath.addAll(configurator.getTaxonPropertyPath());
679
        }
680

    
681

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

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

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

    
702
        if(taxa != null){
703
            results.addAll(taxa);
704
        }
705

    
706
        numberOfResults += numberTaxaResults;
707

    
708
        // Names without taxa
709
        if (configurator.isDoNamesWithoutTaxa()) {
710
            int numberNameResults = 0;
711

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

    
728

    
729

    
730
       return new DefaultPagerImpl<>
731
            (configurator.getPageNumber(), numberOfResults, configurator.getPageSize(), results);
732
    }
733

    
734
    public List<UuidAndTitleCache<TaxonBase>> getTaxonUuidAndTitleCache(Integer limit, String pattern){
735
        return dao.getUuidAndTitleCache(limit, pattern);
736
    }
737

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

    
748
                    //find the best matching representation
749
                    medRep.add(MediaUtils.findBestMatchingRepresentation(media, null, size, height, widthOrDuration, mimeTypes));
750

    
751
                }
752
            }
753
        }
754
        return medRep;
755
    }
756

    
757
    @Override
758
    public List<Media> listTaxonDescriptionMedia(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, boolean limitToGalleries, List<String> propertyPath){
759
        return listMedia(taxon, includeRelationships, limitToGalleries, true, false, false, propertyPath);
760
    }
761

    
762
    @Override
763
    public List<Media> listMedia(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships,
764
            Boolean limitToGalleries, Boolean includeTaxonDescriptions, Boolean includeOccurrences,
765
            Boolean includeTaxonNameDescriptions, List<String> propertyPath) {
766

    
767
    //    logger.setLevel(Level.TRACE);
768
//        Logger.getLogger("org.hibernate.SQL").setLevel(Level.TRACE);
769

    
770
        logger.trace("listMedia() - START");
771

    
772
        Set<Taxon> taxa = new HashSet<>();
773
        List<Media> taxonMedia = new ArrayList<>();
774
        List<Media> nonImageGalleryImages = new ArrayList<>();
775

    
776
        if (limitToGalleries == null) {
777
            limitToGalleries = false;
778
        }
779

    
780
        // --- resolve related taxa
781
        if (includeRelationships != null && ! includeRelationships.isEmpty()) {
782
            logger.trace("listMedia() - resolve related taxa");
783
            taxa = listRelatedTaxa(taxon, includeRelationships, null, null, null, null);
784
        }
785

    
786
        taxa.add((Taxon) dao.load(taxon.getUuid()));
787

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

    
813

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

    
823
//            	direct media removed from specimen #3597
824
//              taxonMedia.addAll(occurrence.getMedia());
825

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

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

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

    
871

    
872
        logger.trace("listMedia() - initialize");
873
        beanInitializer.initializeAll(taxonMedia, propertyPath);
874

    
875
        logger.trace("listMedia() - END");
876

    
877
        return taxonMedia;
878
    }
879

    
880
    @Override
881
    public List<TaxonBase> findTaxaByID(Set<Integer> listOfIDs) {
882
        return this.dao.loadList(listOfIDs, null);
883
    }
884

    
885
    @Override
886
    public TaxonBase findTaxonByUuid(UUID uuid, List<String> propertyPaths){
887
        return this.dao.findByUuid(uuid, null ,propertyPaths);
888
    }
889

    
890
    @Override
891
    public int countSynonyms(boolean onlyAttachedToTaxon){
892
        return this.dao.countSynonyms(onlyAttachedToTaxon);
893
    }
894

    
895
    @Override
896
    public List<TaxonName> findIdenticalTaxonNames(List<String> propertyPath) {
897
        return this.dao.findIdenticalTaxonNames(propertyPath);
898
    }
899

    
900
    @Override
901
    @Transactional(readOnly=false)
902
    public DeleteResult deleteTaxon(UUID taxonUUID, TaxonDeletionConfigurator config, UUID classificationUuid)  {
903

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

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

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

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

    
945
                }
946
            } else{
947
                TaxonDeletionConfigurator configRelTaxon = new TaxonDeletionConfigurator();
948
                configRelTaxon.setDeleteTaxonNodes(false);
949
                configRelTaxon.setDeleteConceptRelationships(true);
950

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

    
958
                        if (taxon.equals(taxRel.getToTaxon()) && isDeletable(taxRel.getFromTaxon().getUuid(), configRelTaxon).isOk()){
959
                            this.deleteTaxon(taxRel.getFromTaxon().getUuid(), configRelTaxon, classificationUuid);
960
                        }else if (isDeletable(taxRel.getToTaxon().getUuid(), configRelTaxon).isOk()){
961
                            this.deleteTaxon(taxRel.getToTaxon().getUuid(), configRelTaxon, classificationUuid);
962
                        }
963
                    }
964
                    taxon.removeTaxonRelation(taxRel);
965

    
966
                }
967
            }
968

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

    
984

    
985
                }
986
                if (result.isOk()){
987
                    for (TaxonDescription desc: removeDescriptions){
988
                        taxon.removeDescription(desc);
989
                        descriptionService.delete(desc);
990
                    }
991
                } else {
992
                    return result;
993
                }
994
            }
995

    
996

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

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

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

    
1087

    
1088
       }
1089
       }
1090

    
1091
        return result;
1092

    
1093
    }
1094

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

    
1102
                return message;
1103
            }
1104

    
1105

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

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

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

    
1124
        }
1125

    
1126
        referencingObjects = null;
1127
        return null;
1128
    }
1129

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

    
1139
    @Override
1140
    @Transactional(readOnly = false)
1141
    public DeleteResult delete(UUID synUUID){
1142
    	DeleteResult result = new DeleteResult();
1143
    	Synonym syn = (Synonym)dao.load(synUUID);
1144

    
1145
        return this.deleteSynonym(syn, null);
1146
    }
1147

    
1148
    @Override
1149
    @Transactional(readOnly = false)
1150
    public DeleteResult deleteSynonym(UUID synonymUuid, SynonymDeletionConfigurator config) {
1151
        return deleteSynonym((Synonym)dao.load(synonymUuid), config);
1152

    
1153
    }
1154

    
1155

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

    
1166
        if (config == null){
1167
            config = new SynonymDeletionConfigurator();
1168
        }
1169

    
1170
        result = isDeletable(synonym.getUuid(), config);
1171

    
1172

    
1173
        if (result.isOk()){
1174

    
1175
            synonym = HibernateProxyHelper.deproxy(this.load(synonym.getUuid()), Synonym.class);
1176

    
1177
            //remove synonym
1178
            Taxon accTaxon = synonym.getAcceptedTaxon();
1179

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

    
1190
            TaxonName name = synonym.getName();
1191
            synonym.setName(null);
1192

    
1193
            dao.delete(synonym);
1194
            result.addDeletedObject(synonym);
1195

    
1196
            //remove name if possible (and required)
1197
            if (name != null && config.isDeleteNameIfPossible()){
1198

    
1199
                    DeleteResult nameDeleteResult = nameService.delete(name, config.getNameDeletionConfig());
1200
                    if (nameDeleteResult.isAbort() || nameDeleteResult.isError()){
1201
                    	result.addExceptions(nameDeleteResult.getExceptions());
1202
                    	result.addRelatedObject(name);
1203
                    }else{
1204
                        result.addDeletedObject(name);
1205
                    }
1206
            }
1207

    
1208
        }
1209
        return result;
1210
    }
1211

    
1212
    @Override
1213
    public List<TaxonName> findIdenticalTaxonNameIds(List<String> propertyPath) {
1214

    
1215
        return this.dao.findIdenticalNamesNew(propertyPath);
1216
    }
1217
//
1218
//    @Override
1219
//    public String getPhylumName(TaxonName name){
1220
//        return this.dao.getPhylumName(name);
1221
//    }
1222

    
1223
    @Override
1224
    public Taxon findBestMatchingTaxon(String taxonName) {
1225
        MatchingTaxonConfigurator config = MatchingTaxonConfigurator.NewInstance();
1226
        config.setTaxonNameTitle(taxonName);
1227
        return findBestMatchingTaxon(config);
1228
    }
1229

    
1230
    @Override
1231
    public Taxon findBestMatchingTaxon(MatchingTaxonConfigurator config) {
1232

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

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

    
1268
                }else{  //not Taxon.class
1269
                    continue;
1270
                }
1271
                countEqualCandidates++;
1272

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

    
1284

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

    
1301
        } catch (Exception e){
1302
            logger.error(e);
1303
            e.printStackTrace();
1304
        }
1305

    
1306
        return bestCandidate;
1307
    }
1308

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

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

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

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

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

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

    
1382
        HomotypicalGroup homotypicGroup = synonymName.getHomotypicalGroup();
1383
        int hgSize = homotypicGroup.getTypifiedNames().size();
1384
        boolean isSingleInGroup = !(hgSize > 1);
1385

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

    
1404
        UpdateResult result = new UpdateResult();
1405
        //move all synonyms to new taxon
1406
        List<Synonym> homotypicSynonyms = oldTaxon.getSynonymsInGroup(homotypicGroup);
1407
        for (Synonym synRelation: homotypicSynonyms){
1408

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

    
1420
            //set result  //why is this needed? Seems wrong to me (AM 10.10.2016)
1421
            if (!synRelation.equals(oldSynonym)){
1422
                result.setError();
1423
            }
1424
        }
1425

    
1426
        result.addUpdatedObject(oldTaxon);
1427
        result.addUpdatedObject(newTaxon);
1428
        saveOrUpdate(oldTaxon);
1429
        saveOrUpdate(newTaxon);
1430

    
1431
        return result;
1432
    }
1433

    
1434
    @Override
1435
    public <T extends TaxonBase> List<UuidAndTitleCache<T>> getUuidAndTitleCache(Class<T> clazz, Integer limit, String pattern) {
1436
        return dao.getUuidAndTitleCache(clazz, limit, pattern);
1437
    }
1438

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

    
1445

    
1446
        LuceneSearch luceneSearch = prepareFindByFullTextSearch(clazz, queryString, classification, languages, highlightFragments, null);
1447

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

    
1458
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1459
        idFieldMap.put(CdmBaseType.TAXON, "id");
1460

    
1461
        // ---  initialize taxa, thighlight matches ....
1462
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1463
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1464
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1465

    
1466
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
1467
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
1468
    }
1469

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

    
1476
        LuceneSearch luceneSearch = prepareByDistributionSearch(areaFilter, statusFilter, classification);
1477

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

    
1488
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1489
        idFieldMap.put(CdmBaseType.TAXON, "id");
1490

    
1491
        // ---  initialize taxa, thighlight matches ....
1492
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1493
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1494
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1495

    
1496
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
1497
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
1498
    }
1499

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

    
1515
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1516
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1517

    
1518
        if(sortFields == null){
1519
            sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING,  false)};
1520
        }
1521
        luceneSearch.setSortFields(sortFields);
1522

    
1523
        // ---- search criteria
1524
        luceneSearch.setCdmTypRestriction(clazz);
1525

    
1526
        if(!queryString.isEmpty() && !queryString.equals("*") && !queryString.equals("?") ) {
1527
            textQueryBuilder.add(taxonBaseQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
1528
            textQueryBuilder.add(taxonBaseQueryFactory.newDefinedTermQuery("name.rank", queryString, languages), Occur.SHOULD);
1529
        }
1530

    
1531
        BooleanQuery textQuery = textQueryBuilder.build();
1532
        if(textQuery.clauses().size() > 0) {
1533
            finalQueryBuilder.add(textQuery, Occur.MUST);
1534
        }
1535

    
1536

    
1537
        if(classification != null){
1538
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery("taxonNodes.classification.id", classification), Occur.MUST);
1539
        }
1540
        luceneSearch.setQuery(finalQueryBuilder.build());
1541

    
1542
        if(highlightFragments){
1543
            luceneSearch.setHighlightFields(taxonBaseQueryFactory.getTextFieldNamesAsArray());
1544
        }
1545
        return luceneSearch;
1546
    }
1547

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

    
1571
        String fromField;
1572
        String queryTermField;
1573
        String toField = "id"; // TaxonBase.uuid
1574

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

    
1588
        Builder finalQueryBuilder = new Builder();
1589

    
1590
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1591
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1592

    
1593
        Builder joinFromQueryBuilder = new Builder();
1594
        joinFromQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(queryTermField, queryString), Occur.MUST);
1595
        joinFromQueryBuilder.add(taxonBaseQueryFactory.newEntityIdsQuery("type.id", edge.getTaxonRelationshipTypes()), Occur.MUST);
1596

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

    
1599
        if(sortFields == null){
1600
            sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING,  false)};
1601
        }
1602
        luceneSearch.setSortFields(sortFields);
1603

    
1604
        finalQueryBuilder.add(joinQuery, Occur.MUST);
1605

    
1606
        if(classification != null){
1607
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery("taxonNodes.classification.id", classification), Occur.MUST);
1608
        }
1609
        luceneSearch.setQuery(finalQueryBuilder.build());
1610

    
1611
        if(highlightFragments){
1612
            luceneSearch.setHighlightFields(taxonBaseQueryFactory.getTextFieldNamesAsArray());
1613
        }
1614
        return luceneSearch;
1615
    }
1616

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

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

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

    
1646
        // set default if parameter is null
1647
        if(searchModes == null){
1648
            searchModes = EnumSet.of(TaxaAndNamesSearchMode.doTaxa);
1649
        }
1650

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

    
1664

    
1665
        boolean addDistributionFilter = namedAreas != null && namedAreas.size() > 0;
1666

    
1667
        List<LuceneSearch> luceneSearches = new ArrayList<>();
1668
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1669

    
1670
        /*
1671
          ======== filtering by distribution , HOWTO ========
1672

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

    
1678

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

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

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

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

    
1711
        Builder multiIndexByAreaFilterBuilder = new Builder();
1712

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

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

    
1746
            }
1747
        }
1748

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

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

    
1774
            luceneSearches.add(byCommonNameSearch);
1775

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

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

    
1807
            if(addDistributionFilter){
1808
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1809

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

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

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

    
1846
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1847
            }
1848
        }
1849

    
1850
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider,
1851
                luceneSearches.toArray(new LuceneSearch[luceneSearches.size()]));
1852

    
1853

    
1854
        if(addDistributionFilter){
1855

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

    
1871
        if (addDistributionFilter){
1872
            multiSearch.setFilter(multiIndexByAreaFilterBuilder.build());
1873
        }
1874

    
1875

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

    
1886
        // --- initialize taxa, highlight matches ....
1887
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
1888

    
1889

    
1890
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1891
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1892

    
1893
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
1894
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
1895
    }
1896

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

    
1912
        String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1913
        String toField = "id"; // id in toType usually this is the TaxonBase index
1914

    
1915
        BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, queryFactory);
1916

    
1917
        ScoreMode scoreMode = asFilter ?  ScoreMode.None : ScoreMode.Max;
1918

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

    
1921
        return taxonAreaJoinQuery;
1922
    }
1923

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

    
1936
        // status field from Distribution
1937
        if(distributionStatusList != null && distributionStatusList.size() > 0){
1938
            areaQueryBuilder.add(queryFactory.newEntityIdsQuery("status.id", distributionStatusList), Occur.MUST);
1939
        }
1940

    
1941
        BooleanQuery areaQuery = areaQueryBuilder.build();
1942
        logger.debug("createByDistributionQuery() query: " + areaQuery.toString());
1943
        return areaQuery;
1944
    }
1945

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

    
1961
        Builder finalQueryBuilder = new Builder();
1962

    
1963
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
1964

    
1965
        // FIXME is this query factory using the wrong type?
1966
        QueryFactory taxonQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Taxon.class);
1967

    
1968
        SortField[] sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING, false)};
1969
        luceneSearch.setSortFields(sortFields);
1970

    
1971

    
1972
        Query byAreaQuery = createByDistributionJoinQuery(namedAreaList, distributionStatusList, taxonQueryFactory, null, false);
1973

    
1974
        finalQueryBuilder.add(byAreaQuery, Occur.MUST);
1975

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

    
1983
        return luceneSearch;
1984
    }
1985

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

    
1992

    
1993
        LuceneSearch luceneSearch = prepareByDescriptionElementFullTextSearch(clazz, queryString, classification, features, languages, highlightFragments);
1994

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

    
2005
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
2006
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2007

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

    
2014
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
2015
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
2016

    
2017
    }
2018

    
2019

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

    
2025
        LuceneSearch luceneSearchByDescriptionElement = prepareByDescriptionElementFullTextSearch(null, queryString, classification, null, languages, highlightFragments);
2026
        LuceneSearch luceneSearchByTaxonBase = prepareFindByFullTextSearch(null, queryString, classification, languages, highlightFragments, null);
2027

    
2028
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider, luceneSearchByDescriptionElement, luceneSearchByTaxonBase);
2029

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

    
2040
        // --- initialize taxa, highlight matches ....
2041
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
2042

    
2043
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
2044
        idFieldMap.put(CdmBaseType.TAXON, "id");
2045
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2046

    
2047
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2048
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2049

    
2050
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
2051
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
2052

    
2053
    }
2054

    
2055

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

    
2070
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, DescriptionElementBase.class);
2071
        QueryFactory descriptionElementQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(DescriptionElementBase.class);
2072

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

    
2075
        BooleanQuery finalQuery = createByDescriptionElementFullTextQuery(queryString, classification, features,
2076
                languages, descriptionElementQueryFactory);
2077

    
2078
        luceneSearch.setSortFields(sortFields);
2079
        luceneSearch.setCdmTypRestriction(clazz);
2080
        luceneSearch.setQuery(finalQuery);
2081
        if(highlightFragments){
2082
            luceneSearch.setHighlightFields(descriptionElementQueryFactory.getTextFieldNamesAsArray());
2083
        }
2084

    
2085
        return luceneSearch;
2086
    }
2087

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

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

    
2116

    
2117
        // text field from TextData
2118
        textQueryBuilder.add(descriptionElementQueryFactory.newMultilanguageTextQuery("text", queryString, languages), Occur.SHOULD);
2119

    
2120
        // --- TermBase fields - by representation ----
2121
        // state field from CategoricalData
2122
        textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.state", queryString, languages), Occur.SHOULD);
2123

    
2124
        // state field from CategoricalData
2125
        textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.modifyingText", queryString, languages), Occur.SHOULD);
2126

    
2127
        // area field from Distribution
2128
        textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("area", queryString, languages), Occur.SHOULD);
2129

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

    
2133
        finalQueryBuilder.add(textQueryBuilder.build(), Occur.MUST);
2134
        // --- classification ----
2135

    
2136
        if(classification != null){
2137
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityIdQuery("inDescription.taxon.taxonNodes.classification.id", classification), Occur.MUST);
2138
        }
2139

    
2140
        // --- IdentifieableEntity fields - by uuid
2141
        if(features != null && features.size() > 0 ){
2142
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityUuidsQuery("feature.uuid", features), Occur.MUST);
2143
        }
2144

    
2145
        // the description must be associated with a taxon
2146
        finalQueryBuilder.add(descriptionElementQueryFactory.newIsNotNullQuery("inDescription.taxon.id"), Occur.MUST);
2147

    
2148
        BooleanQuery finalQuery = finalQueryBuilder.build();
2149
        logger.info("prepareByDescriptionElementFullTextSearch() query: " + finalQuery.toString());
2150
        return finalQuery;
2151
    }
2152

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

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

    
2181
    @Override
2182
    public List<Synonym> createInferredSynonyms(Taxon taxon, Classification classification, SynonymType type, boolean doWithMisappliedNames){
2183
        List <Synonym> inferredSynonyms = new ArrayList<>();
2184
        List<Synonym> inferredSynonymsToBeRemoved = new ArrayList<>();
2185

    
2186
        HashMap <UUID, IZoologicalName> zooHashMap = new HashMap<>();
2187

    
2188

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

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

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

    
2218

    
2219
                    //create inferred synonyms for species, subspecies
2220
                    if ((parentName.isGenus() || parentName.isSpecies() || parentName.getRank().equals(Rank.SUBGENUS())) ){
2221

    
2222
                        Synonym inferredEpithet = null;
2223
                        Synonym inferredGenus = null;
2224
                        Synonym potentialCombination = null;
2225

    
2226
                        List<String> propertyPaths = new ArrayList<>();
2227
                        propertyPaths.add("synonym");
2228
                        propertyPaths.add("synonym.name");
2229
                        List<OrderHint> orderHintsSynonyms = new ArrayList<>();
2230
                        orderHintsSynonyms.add(new OrderHint("titleCache", SortOrder.ASCENDING));
2231

    
2232
                        List<Synonym> synonyMsOfParent = dao.getSynonyms(parentTaxon, SynonymType.HETEROTYPIC_SYNONYM_OF(), null, null,orderHintsSynonyms,propertyPaths);
2233
                        List<Synonym> synonymsOfTaxon= dao.getSynonyms(taxon, SynonymType.HETEROTYPIC_SYNONYM_OF(),
2234
                                null, null,orderHintsSynonyms,propertyPaths);
2235

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

    
2247
                        if (type.equals(SynonymType.INFERRED_EPITHET_OF())){
2248
                            for (Synonym synonymRelationOfParent:synonyMsOfParent){
2249

    
2250
                                inferredEpithet = createInferredEpithets(taxon,
2251
                                        zooHashMap, taxonName, epithetOfTaxon,
2252
                                        infragenericEpithetOfTaxon,
2253
                                        infraspecificEpithetOfTaxon,
2254
                                        taxonNames, parentName,
2255
                                        synonymRelationOfParent);
2256

    
2257
                                inferredSynonyms.add(inferredEpithet);
2258
                                zooHashMap.put(inferredEpithet.getName().getUuid(), inferredEpithet.getName());
2259
                                taxonNames.add(inferredEpithet.getName().getNameCache());
2260
                            }
2261

    
2262
                            if (doWithMisappliedNames){
2263

    
2264
                                for (TaxonRelationship taxonRelationship: taxonRelListParent){
2265
                                     Taxon misappliedName = taxonRelationship.getFromTaxon();
2266

    
2267
                                     inferredEpithet = createInferredEpithets(taxon,
2268
                                             zooHashMap, taxonName, epithetOfTaxon,
2269
                                             infragenericEpithetOfTaxon,
2270
                                             infraspecificEpithetOfTaxon,
2271
                                             taxonNames, parentName,
2272
                                             misappliedName);
2273

    
2274
                                    inferredSynonyms.add(inferredEpithet);
2275
                                    zooHashMap.put(inferredEpithet.getName().getUuid(), inferredEpithet.getName());
2276
                                    taxonNames.add(inferredEpithet.getName().getNameCache());
2277
                                }
2278
                            }
2279

    
2280
                            if (!taxonNames.isEmpty()){
2281
                            List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2282
                            IZoologicalName name;
2283
                            if (!synNotInCDM.isEmpty()){
2284
                                inferredSynonymsToBeRemoved.clear();
2285

    
2286
                                for (Synonym syn :inferredSynonyms){
2287
                                    name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2288
                                    if (!synNotInCDM.contains(name.getNameCache())){
2289
                                        inferredSynonymsToBeRemoved.add(syn);
2290
                                    }
2291
                                }
2292

    
2293
                                // Remove identified Synonyms from inferredSynonyms
2294
                                for (Synonym synonym : inferredSynonymsToBeRemoved) {
2295
                                    inferredSynonyms.remove(synonym);
2296
                                }
2297
                            }
2298
                        }
2299

    
2300
                    }else if (type.equals(SynonymType.INFERRED_GENUS_OF())){
2301

    
2302
                        for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2303

    
2304
                            inferredGenus = createInferredGenus(taxon,
2305
                                    zooHashMap, taxonName, epithetOfTaxon,
2306
                                    genusOfTaxon, taxonNames, zooParentName, synonymRelationOfTaxon);
2307

    
2308
                            inferredSynonyms.add(inferredGenus);
2309
                            zooHashMap.put(inferredGenus.getName().getUuid(), inferredGenus.getName());
2310
                            taxonNames.add(inferredGenus.getName().getNameCache());
2311
                        }
2312

    
2313
                        if (doWithMisappliedNames){
2314

    
2315
                            for (TaxonRelationship taxonRelationship: taxonRelListTaxon){
2316
                                Taxon misappliedName = taxonRelationship.getFromTaxon();
2317
                                inferredGenus = createInferredGenus(taxon, zooHashMap, taxonName, infraspecificEpithetOfTaxon, genusOfTaxon, taxonNames, zooParentName,  misappliedName);
2318

    
2319
                                inferredSynonyms.add(inferredGenus);
2320
                                zooHashMap.put(inferredGenus.getName().getUuid(), inferredGenus.getName());
2321
                                 taxonNames.add(inferredGenus.getName().getNameCache());
2322
                            }
2323
                        }
2324

    
2325

    
2326
                        if (!taxonNames.isEmpty()){
2327
                            List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2328
                            IZoologicalName name;
2329
                            if (!synNotInCDM.isEmpty()){
2330
                                inferredSynonymsToBeRemoved.clear();
2331

    
2332
                                for (Synonym syn :inferredSynonyms){
2333
                                    name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2334
                                    if (!synNotInCDM.contains(name.getNameCache())){
2335
                                        inferredSynonymsToBeRemoved.add(syn);
2336
                                    }
2337
                                }
2338

    
2339
                                // Remove identified Synonyms from inferredSynonyms
2340
                                for (Synonym synonym : inferredSynonymsToBeRemoved) {
2341
                                    inferredSynonyms.remove(synonym);
2342
                                }
2343
                            }
2344
                        }
2345

    
2346
                    }else if (type.equals(SynonymType.POTENTIAL_COMBINATION_OF())){
2347

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

    
2355
                            synName = synonymRelationOfParent.getName();
2356

    
2357
                            // Set the sourceReference
2358
                            sourceReference = synonymRelationOfParent.getSec();
2359

    
2360
                            // Determine the idInSource
2361
                            String idInSourceParent = getIdInSource(synonymRelationOfParent);
2362

    
2363
                            IZoologicalName parentSynZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2364
                            String synParentGenus = parentSynZooName.getGenusOrUninomial();
2365
                            String synParentInfragenericName = null;
2366
                            String synParentSpecificEpithet = null;
2367

    
2368
                            if (parentSynZooName.isInfraGeneric()){
2369
                                synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2370
                            }
2371
                            if (parentSynZooName.isSpecies()){
2372
                                synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2373
                            }
2374

    
2375
                           /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2376
                                synonymsGenus.put(synGenusName, idInSource);
2377
                            }*/
2378

    
2379
                            //for all synonyms of the taxon
2380

    
2381
                            for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2382

    
2383
                                IZoologicalName zooSynName = getZoologicalName(synonymRelationOfTaxon.getName().getUuid(), zooHashMap);
2384
                                potentialCombination = createPotentialCombination(idInSourceParent, parentSynZooName, zooSynName,
2385
                                        synParentGenus,
2386
                                        synParentInfragenericName,
2387
                                        synParentSpecificEpithet, synonymRelationOfTaxon, zooHashMap);
2388

    
2389
                                taxon.addSynonym(potentialCombination, SynonymType.POTENTIAL_COMBINATION_OF());
2390
                                inferredSynonyms.add(potentialCombination);
2391
                                zooHashMap.put(potentialCombination.getName().getUuid(), potentialCombination.getName());
2392
                                 taxonNames.add(potentialCombination.getName().getNameCache());
2393

    
2394
                            }
2395

    
2396
                        }
2397

    
2398
                        if (doWithMisappliedNames){
2399

    
2400
                            for (TaxonRelationship parentRelationship: taxonRelListParent){
2401

    
2402
                                TaxonName misappliedParentName;
2403

    
2404
                                Taxon misappliedParent = parentRelationship.getFromTaxon();
2405
                                misappliedParentName = misappliedParent.getName();
2406

    
2407
                                HibernateProxyHelper.deproxy(misappliedParent);
2408

    
2409
                                // Set the sourceReference
2410
                                sourceReference = misappliedParent.getSec();
2411

    
2412
                                // Determine the idInSource
2413
                                String idInSourceParent = getIdInSource(misappliedParent);
2414

    
2415
                                IZoologicalName parentSynZooName = getZoologicalName(misappliedParentName.getUuid(), zooHashMap);
2416
                                String synParentGenus = parentSynZooName.getGenusOrUninomial();
2417
                                String synParentInfragenericName = null;
2418
                                String synParentSpecificEpithet = null;
2419

    
2420
                                if (parentSynZooName.isInfraGeneric()){
2421
                                    synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2422
                                }
2423
                                if (parentSynZooName.isSpecies()){
2424
                                    synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2425
                                }
2426

    
2427

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

    
2437

    
2438
                                    taxon.addSynonym(potentialCombination, SynonymType.POTENTIAL_COMBINATION_OF());
2439
                                    inferredSynonyms.add(potentialCombination);
2440
                                    zooHashMap.put(potentialCombination.getName().getUuid(), potentialCombination.getName());
2441
                                     taxonNames.add(potentialCombination.getName().getNameCache());
2442
                                }
2443
                            }
2444
                        }
2445

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

    
2475
        }
2476

    
2477
        return inferredSynonyms;
2478
    }
2479

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

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

    
2497
                sourceReference = taxon.getSec();
2498
            }
2499
        }
2500
        String synTaxonSpecificEpithet = zooSynName.getSpecificEpithet();
2501

    
2502
        String synTaxonInfraSpecificName= null;
2503

    
2504
        if (parentSynZooName.isSpecies()){
2505
            synTaxonInfraSpecificName = zooSynName.getInfraSpecificEpithet();
2506
        }
2507

    
2508
        /*if (epithetName != null && !synonymsEpithet.contains(epithetName)){
2509
            synonymsEpithet.add(epithetName);
2510
        }*/
2511

    
2512
        //create potential combinations...
2513
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(syn.getName().getRank());
2514

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

    
2530

    
2531
        potentialCombination = Synonym.NewInstance(inferredSynName, null);
2532

    
2533
        // Set the sourceReference
2534
        potentialCombination.setSec(sourceReference);
2535

    
2536

    
2537
        // Determine the idInSource
2538
        String idInSourceSyn= getIdInSource(syn);
2539

    
2540
        if (idInSourceParent != null && idInSourceSyn != null) {
2541
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2542
            inferredSynName.addSource(originalSource);
2543
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2544
            potentialCombination.addSource(originalSource);
2545
        }
2546

    
2547
        return potentialCombination;
2548
    }
2549

    
2550
    private Synonym createInferredGenus(Taxon taxon,
2551
            Map<UUID, IZoologicalName> zooHashMap, IZoologicalName taxonName,
2552
            String epithetOfTaxon, String genusOfTaxon,
2553
            List<String> taxonNames, IZoologicalName zooParentName,
2554
            TaxonBase syn) {
2555

    
2556
        Synonym inferredGenus;
2557
        TaxonName synName;
2558
        IZoologicalName inferredSynName;
2559
        synName =syn.getName();
2560
        HibernateProxyHelper.deproxy(syn);
2561

    
2562
        // Determine the idInSource
2563
        String idInSourceSyn = getIdInSource(syn);
2564
        String idInSourceTaxon = getIdInSource(taxon);
2565
        // Determine the sourceReference
2566
        Reference sourceReference = syn.getSec();
2567

    
2568
        //logger.warn(sourceReference.getTitleCache());
2569

    
2570
        synName = syn.getName();
2571
        IZoologicalName synZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2572
        String synSpeciesEpithetName = synZooName.getSpecificEpithet();
2573
                     /* if (synonymsEpithet != null && !synonymsEpithet.contains(synSpeciesEpithetName)){
2574
            synonymsEpithet.add(synSpeciesEpithetName);
2575
        }*/
2576

    
2577
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2578
        //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...
2579

    
2580

    
2581
        inferredSynName.setGenusOrUninomial(genusOfTaxon);
2582
        if (zooParentName.isInfraGeneric()){
2583
            inferredSynName.setInfraGenericEpithet(zooParentName.getInfraGenericEpithet());
2584
        }
2585

    
2586
        if (taxonName.isSpecies()){
2587
            inferredSynName.setSpecificEpithet(synSpeciesEpithetName);
2588
        }
2589
        if (taxonName.isInfraSpecific()){
2590
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2591
            inferredSynName.setInfraSpecificEpithet(synZooName.getInfraGenericEpithet());
2592
        }
2593

    
2594

    
2595
        inferredGenus = Synonym.NewInstance(inferredSynName, null);
2596

    
2597
        // Set the sourceReference
2598
        inferredGenus.setSec(sourceReference);
2599

    
2600
        // Add the original source
2601
        if (idInSourceSyn != null && idInSourceTaxon != null) {
2602
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2603
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2604
            inferredGenus.addSource(originalSource);
2605

    
2606
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2607
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2608
            inferredSynName.addSource(originalSource);
2609
            originalSource = null;
2610

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

    
2617
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2618
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2619
            inferredSynName.addSource(originalSource);
2620
            originalSource = null;
2621
        }
2622

    
2623
        taxon.addSynonym(inferredGenus, SynonymType.INFERRED_GENUS_OF());
2624

    
2625
        return inferredGenus;
2626
    }
2627

    
2628
    private Synonym createInferredEpithets(Taxon taxon,
2629
            Map<UUID, IZoologicalName> zooHashMap, IZoologicalName taxonName,
2630
            String epithetOfTaxon, String infragenericEpithetOfTaxon,
2631
            String infraspecificEpithetOfTaxon, List<String> taxonNames,
2632
            TaxonName parentName, TaxonBase<?> syn) {
2633

    
2634
        Synonym inferredEpithet;
2635
        TaxonName synName;
2636
        IZoologicalName inferredSynName;
2637
        HibernateProxyHelper.deproxy(syn);
2638

    
2639
        // Determine the idInSource
2640
        String idInSourceSyn = getIdInSource(syn);
2641
        String idInSourceTaxon =  getIdInSource(taxon);
2642
        // Determine the sourceReference
2643
        Reference sourceReference = syn.getSec();
2644

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

    
2650
        synName = syn.getName();
2651
        IZoologicalName zooSynName = getZoologicalName(synName.getUuid(), zooHashMap);
2652
        String synGenusName = zooSynName.getGenusOrUninomial();
2653
        String synInfraGenericEpithet = null;
2654
        String synSpecificEpithet = null;
2655

    
2656
        if (zooSynName.getInfraGenericEpithet() != null){
2657
            synInfraGenericEpithet = zooSynName.getInfraGenericEpithet();
2658
        }
2659

    
2660
        if (zooSynName.isInfraSpecific()){
2661
            synSpecificEpithet = zooSynName.getSpecificEpithet();
2662
        }
2663

    
2664
                     /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2665
            synonymsGenus.put(synGenusName, idInSource);
2666
        }*/
2667

    
2668
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2669

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

    
2676
        if (parentName.isInfraGeneric()){
2677
            inferredSynName.setInfraGenericEpithet(synInfraGenericEpithet);
2678
        }
2679
        if (taxonName.isSpecies()){
2680
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2681
        }else if (taxonName.isInfraSpecific()){
2682
            inferredSynName.setSpecificEpithet(synSpecificEpithet);
2683
            inferredSynName.setInfraSpecificEpithet(infraspecificEpithetOfTaxon);
2684
        }
2685

    
2686
        inferredEpithet = Synonym.NewInstance(inferredSynName, null);
2687

    
2688
        // Set the sourceReference
2689
        inferredEpithet.setSec(sourceReference);
2690

    
2691
        /* Add the original source
2692
        if (idInSource != null) {
2693
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(idInSource, "InferredEpithetOf", syn.getSec(), null);
2694

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

    
2704

    
2705
        IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2706
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2707

    
2708
        inferredEpithet.addSource(originalSource);
2709

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

    
2713
        inferredSynName.addSource(originalSource);
2714

    
2715

    
2716

    
2717
        taxon.addSynonym(inferredEpithet, SynonymType.INFERRED_EPITHET_OF());
2718

    
2719
        return inferredEpithet;
2720
    }
2721

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

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

    
2763

    
2764
        return idInSource;
2765
    }
2766

    
2767

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

    
2784
        return citation;
2785
    }
2786

    
2787
    @Override
2788
    public List<Synonym>  createAllInferredSynonyms(Taxon taxon, Classification tree, boolean doWithMisappliedNames){
2789
        List <Synonym> inferredSynonyms = new ArrayList<>();
2790

    
2791
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.INFERRED_EPITHET_OF(), doWithMisappliedNames));
2792
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.INFERRED_GENUS_OF(), doWithMisappliedNames));
2793
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.POTENTIAL_COMBINATION_OF(), doWithMisappliedNames));
2794

    
2795
        return inferredSynonyms;
2796
    }
2797

    
2798
    @Override
2799
    public List<Classification> listClassifications(TaxonBase taxonBase, Integer limit, Integer start, List<String> propertyPaths) {
2800

    
2801
        // TODO quickly implemented, create according dao !!!!
2802
        Set<TaxonNode> nodes = new HashSet<>();
2803
        Set<Classification> classifications = new HashSet<>();
2804
        List<Classification> list = new ArrayList<>();
2805

    
2806
        if (taxonBase == null) {
2807
            return list;
2808
        }
2809

    
2810
        taxonBase = load(taxonBase.getUuid());
2811

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

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

    
2838
        result.addUpdatedObject(fromTaxon);
2839
        result.addUpdatedObject(toTaxon);
2840
        result.addUpdatedObject(result.getCdmEntity());
2841

    
2842
        return result;
2843
    }
2844

    
2845
    @Override
2846
    @Transactional(readOnly = false)
2847
    public UpdateResult changeRelatedTaxonToSynonym(Taxon fromTaxon, Taxon toTaxon, TaxonRelationshipType oldRelationshipType,
2848
            SynonymType synonymType) throws DataChangeNoRollbackException {
2849

    
2850
        UpdateResult result = new UpdateResult();
2851
        // Create new synonym using concept name
2852
        TaxonName synonymName = fromTaxon.getName();
2853

    
2854
        // Remove concept relation from taxon
2855
        toTaxon.removeTaxon(fromTaxon, oldRelationshipType);
2856

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

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

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

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

    
2905
        return result;
2906
    }
2907

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

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

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

    
2940

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

    
2947
                //TaxonInteraction
2948
                if (ref.isInstanceOf(TaxonInteraction.class)){
2949
                    message = "Taxon can't be deleted as it is used in taxonInteraction#taxon2";
2950
                }
2951

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

    
2964
        return result;
2965
    }
2966

    
2967
    @Override
2968
    public IncludedTaxaDTO listIncludedTaxa(UUID taxonUuid, IncludedTaxonConfiguration config) {
2969
        IncludedTaxaDTO result = new IncludedTaxaDTO(taxonUuid);
2970

    
2971
        //preliminary implementation
2972

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

    
2989
        Set<Taxon> related = makeRelatedIncluded(taxa, result, config);
2990
        int i = 0;
2991
        while((! related.isEmpty()) && i++ < 100){  //to avoid
2992
             related = makeRelatedIncluded(related, result, config);
2993
        }
2994

    
2995
        return result;
2996
    }
2997

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

    
3011
        //children
3012
        Set<TaxonNode> taxonNodes = new HashSet<>();
3013
        for (Taxon taxon: uncheckedTaxa){
3014
            taxonNodes.addAll(taxon.getTaxonNodes());
3015
        }
3016

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

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

    
3038
        //concept relations
3039
        Set<Taxon> uncheckedAndChildren = new HashSet<>(uncheckedTaxa);
3040
        uncheckedAndChildren.addAll(children);
3041

    
3042
        Set<Taxon> relatedTaxa = makeConceptIncludedTaxa(uncheckedAndChildren, existingTaxa, config);
3043

    
3044

    
3045
        Set<Taxon> result = new HashSet<>(relatedTaxa);
3046
        return result;
3047
    }
3048

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

    
3056
        for (Taxon taxon : unchecked){
3057
            Set<TaxonRelationship> fromRelations = taxon.getRelationsFromThisTaxon();
3058
            Set<TaxonRelationship> toRelations = taxon.getRelationsToThisTaxon();
3059

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

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

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

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

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

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

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

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

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

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

    
3157
    @Override
3158
	@Transactional(readOnly = false)
3159
	public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym, UUID newTaxonUUID, boolean moveHomotypicGroup,
3160
            SynonymType newSynonymType, Reference newSecundum, String newSecundumDetail,
3161
            boolean keepSecundumIfUndefined) throws HomotypicalGroupChangeException {
3162

    
3163
	    UpdateResult result = new UpdateResult();
3164
		Taxon newTaxon = CdmBase.deproxy(dao.load(newTaxonUUID),Taxon.class);
3165
		result = moveSynonymToAnotherTaxon(oldSynonym, newTaxon, moveHomotypicGroup, newSynonymType,
3166
		        newSecundum, newSecundumDetail, keepSecundumIfUndefined);
3167

    
3168
		return result;
3169
	}
3170

    
3171
	@Override
3172
	public UpdateResult moveFactualDateToAnotherTaxon(UUID fromTaxonUuid, UUID toTaxonUuid){
3173
		UpdateResult result = new UpdateResult();
3174

    
3175
		Taxon fromTaxon = (Taxon)dao.load(fromTaxonUuid);
3176
		Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
3177
    	for(TaxonDescription description : fromTaxon.getDescriptions()){
3178
              //reload to avoid session conflicts
3179
              description = HibernateProxyHelper.deproxy(description, TaxonDescription.class);
3180

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

    
3198
        }
3199

    
3200
    	return result;
3201
	}
3202

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

    
3212
		return this.swapSynonymAndAcceptedTaxon(syn, taxon);
3213
	}
3214

    
3215

    
3216
}
(98-98/105)