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
 * @date 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
        UpdateResult result = new UpdateResult();
318
        // Get name from synonym
319
        if (synonym == null){
320
            return null;
321
        }
322
        TaxonName synonymName = synonym.getName();
323

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

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

    
339
        return result;
340
    }
341

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

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

    
354
        //remove existing basionym relationships
355
        synonymName.removeBasionyms();
356

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

    
365
        //set synonym relationship correctly
366
        Taxon acceptedTaxon = synonym.getAcceptedTaxon();
367

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

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

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

    
388
    }
389

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

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

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

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

    
417
        return new DefaultPagerImpl<TaxonBase>(pageNumber, numberOfResults, pageSize, results);
418
    }
419

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

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

    
432
        return results;
433
    }
434

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

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

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

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

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

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

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

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

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

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

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

    
494
        Taxon result = null;
495
        Long count = 0l;
496

    
497
        Synonym synonym = null;
498

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

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

    
516
            }
517
        }
518

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

    
524
        return result;
525
    }
526

    
527

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

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

    
538

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

    
551
        if(taxa.isEmpty()) {
552
            taxa.add(taxon);
553
        }
554

    
555
        if(includeRelationships.isEmpty()){
556
            return taxa;
557
        }
558

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

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

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

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

    
608
        return new DefaultPagerImpl<Synonym>(pageNumber, numberOfResults, pageSize, results);
609
    }
610

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

    
617

    
618
        //homotypic
619
        result.add(taxon.getHomotypicSynonymsByHomotypicGroup(comparator));
620

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

    
627
        return result;
628

    
629
    }
630

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

    
636
        return t.getHomotypicSynonymsByHomotypicGroup(comparator);
637
    }
638

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

    
650
    @Override
651
    public List<UuidAndTitleCache<IdentifiableEntity>> findTaxaAndNamesForEditor(IFindTaxaAndNamesConfigurator configurator){
652

    
653
        List<UuidAndTitleCache<IdentifiableEntity>> results = new ArrayList<UuidAndTitleCache<IdentifiableEntity>>();
654

    
655

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

    
660
        return results;
661
    }
662

    
663
    @Override
664
    public Pager<IdentifiableEntity> findTaxaAndNames(IFindTaxaAndNamesConfigurator configurator) {
665

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

    
670
        // Taxa and synonyms
671
        long numberTaxaResults = 0L;
672

    
673

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

    
679

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

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

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

    
700
        if(taxa != null){
701
            results.addAll(taxa);
702
        }
703

    
704
        numberOfResults += numberTaxaResults;
705

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

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

    
726

    
727

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

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

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

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

    
749
                }
750
            }
751
        }
752
        return medRep;
753
    }
754

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

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

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

    
768
        logger.trace("listMedia() - START");
769

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

    
774
        if (limitToGalleries == null) {
775
            limitToGalleries = false;
776
        }
777

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

    
784
        taxa.add((Taxon) dao.load(taxon.getUuid()));
785

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

    
811

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

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

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

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

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

    
869

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

    
873
        logger.trace("listMedia() - END");
874

    
875
        return taxonMedia;
876
    }
877

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

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

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

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

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

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

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

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

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

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

    
949
                for (TaxonRelationship taxRel: taxon.getTaxonRelations()){
950
                    if (config.isDeleteMisappliedNamesAndInvalidDesignations()){
951
                        if (taxRel.getType().equals(TaxonRelationshipType.MISAPPLIED_NAME_FOR()) || taxRel.getType().equals(TaxonRelationshipType.INVALID_DESIGNATION_FOR())){
952
                            if (taxon.equals(taxRel.getToTaxon())){
953
                                this.deleteTaxon(taxRel.getFromTaxon().getUuid(), config, classificationUuid);
954
                            }
955
                        }
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<TaxonDescription>();
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
                    } else {
1024
                    	result.setError();
1025
                    	result.addException(new Exception("The taxon can not be deleted because it is not used in defined classification."));
1026
                    }
1027
                } else if (config.isDeleteInAllClassifications()){
1028
                    List<TaxonNode> nodesList = new ArrayList<TaxonNode>();
1029
                    nodesList.addAll(taxon.getTaxonNodes());
1030
                    for (ITaxonTreeNode treeNode: nodesList){
1031
                        TaxonNode taxonNode = (TaxonNode) treeNode;
1032
                        if(!deleteChildren){
1033
                            Object[] childNodes = taxonNode.getChildNodes().toArray();
1034
                            for (Object childNode: childNodes){
1035
                                TaxonNode childNodeCast = (TaxonNode) childNode;
1036
                                taxonNode.getParent().addChildNode(childNodeCast, childNodeCast.getReference(), childNodeCast.getMicroReference());
1037
                            }
1038
                        }
1039
                    }
1040
                    config.getTaxonNodeConfig().setDeleteElement(false);
1041
                    DeleteResult resultNodes = nodeService.deleteTaxonNodes(nodesList, config);
1042
                    if (!resultNodes.isOk()){
1043
                    	result.addExceptions(resultNodes.getExceptions());
1044
                    	result.setStatus(resultNodes.getStatus());
1045
                    } else {
1046
                        result.addUpdatedObjects(resultNodes.getUpdatedObjects());
1047
                    }
1048
                }
1049
                if (!success){
1050
                    result.setError();
1051
                    result.addException(new Exception("The taxon can not be deleted because the taxon node can not be removed."));
1052
                }
1053
            }
1054
         }
1055
         TaxonName name = taxon.getName();
1056
         taxon.setName(null);
1057
         this.saveOrUpdate(taxon);
1058

    
1059
         if ((taxon.getTaxonNodes() == null || taxon.getTaxonNodes().size()== 0)  && result.isOk()){
1060
             try{
1061
                 //taxon.setName(null);
1062
                 UUID uuid = dao.delete(taxon);
1063

    
1064
             }catch(Exception e){
1065
                 result.addException(e);
1066
                 result.setError();
1067

    
1068
             }
1069
         } else {
1070
             result.setError();
1071
             result.addException(new Exception("The Taxon can't be deleted because it is used in a classification."));
1072

    
1073
         }
1074
            //TaxonName
1075
        if (config.isDeleteNameIfPossible() && result.isOk()){
1076
           // name = HibernateProxyHelper.deproxy(name);
1077

    
1078
            DeleteResult nameResult = new DeleteResult();
1079
            //remove name if possible (and required)
1080
            if (name != null ){
1081
                nameResult = nameService.delete(name.getUuid(), config.getNameDeletionConfig());
1082
            }
1083
            if (nameResult.isError() || nameResult.isAbort()){
1084
                result.addRelatedObject(name);
1085
                result.addExceptions(nameResult.getExceptions());
1086
            }
1087

    
1088

    
1089
            }
1090

    
1091

    
1092

    
1093

    
1094

    
1095
        }
1096

    
1097
        return result;
1098

    
1099
    }
1100

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

    
1108
                return message;
1109
            }
1110

    
1111

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

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

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

    
1130
        }
1131

    
1132
        referencingObjects = null;
1133
        return null;
1134
    }
1135

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

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

    
1151
        return this.deleteSynonym(syn, null);
1152
    }
1153

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

    
1159
    }
1160

    
1161

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

    
1172
        if (config == null){
1173
            config = new SynonymDeletionConfigurator();
1174
        }
1175

    
1176
        result = isDeletable(synonym.getUuid(), config);
1177

    
1178

    
1179
        if (result.isOk()){
1180

    
1181
            synonym = HibernateProxyHelper.deproxy(this.load(synonym.getUuid()), Synonym.class);
1182

    
1183
            //remove synonym
1184
            Taxon accTaxon = synonym.getAcceptedTaxon();
1185

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

    
1196
            TaxonName name = synonym.getName();
1197
            synonym.setName(null);
1198

    
1199
            dao.delete(synonym);
1200

    
1201

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

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

    
1212
        }
1213
        return result;
1214
    }
1215

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

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

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

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

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

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

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

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

    
1288

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

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

    
1310
        return bestCandidate;
1311
    }
1312

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

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

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

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

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

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

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

    
1390
        if (! isSingleInGroup){
1391
            boolean isHomotypicToAccepted = synonymName.isHomotypic(fromTaxonName);
1392
            boolean hasHomotypicSynonymRelatives = isHomotypicToAccepted ? hgSize > 2 : hgSize > 1;
1393
            if (isHomotypicToAccepted){
1394
                String message = "Synonym is in homotypic group with accepted taxon%s. First remove synonym from homotypic group of accepted taxon before moving to other taxon.";
1395
                String homotypicRelatives = hasHomotypicSynonymRelatives ? " and other synonym(s)":"";
1396
                message = String.format(message, homotypicRelatives);
1397
                throw new HomotypicalGroupChangeException(message);
1398
            }
1399
            if (! moveHomotypicGroup){
1400
                String message = "Synonym is in homotypic group with other synonym(s). Either move complete homotypic group or remove synonym from homotypic group prior to moving to other taxon.";
1401
                throw new HomotypicalGroupChangeException(message);
1402
            }
1403
        }else{
1404
            moveHomotypicGroup = true;  //single synonym always allows to moveCompleteGroup
1405
        }
1406
//        Assert.assertTrue("Synonym can only be moved with complete homotypic group", moveHomotypicGroup);
1407

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

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

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

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

    
1435
        return result;
1436
    }
1437

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

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

    
1449

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1540

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

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

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

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

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

    
1592
        Builder finalQueryBuilder = new Builder();
1593

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

    
1597
        Builder joinFromQueryBuilder = new Builder();
1598
        joinFromQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(queryTermField, queryString), Occur.MUST);
1599
        joinFromQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery("type.id", edge.getTaxonRelationshipType()), Occur.MUST);
1600
        Query joinQuery = taxonBaseQueryFactory.newJoinQuery(TaxonRelationship.class, fromField, false, joinFromQueryBuilder.build(), toField, null, ScoreMode.Max);
1601

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

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

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

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

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

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

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

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

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

    
1667

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

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

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

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

    
1681

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

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

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

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

    
1714
        Builder multiIndexByAreaFilterBuilder = new Builder();
1715

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

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

    
1749
            }
1750
        }
1751

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

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

    
1777
            luceneSearches.add(byCommonNameSearch);
1778

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

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

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

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

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

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

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

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

    
1856

    
1857
        if(addDistributionFilter){
1858

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

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

    
1878

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

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

    
1892

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

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

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

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

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

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

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

    
1924
        return taxonAreaJoinQuery;
1925
    }
1926

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

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

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

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

    
1964
        Builder finalQueryBuilder = new Builder();
1965

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

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

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

    
1974

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

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

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

    
1986
        return luceneSearch;
1987
    }
1988

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

    
1995

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

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

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

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

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

    
2020
    }
2021

    
2022

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

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

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

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

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

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

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

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

    
2056
    }
2057

    
2058

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

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

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

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

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

    
2088
        return luceneSearch;
2089
    }
2090

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

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

    
2119

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
2191

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

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

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

    
2221

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

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

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

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

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

    
2247
                        if (type.equals(SynonymType.INFERRED_EPITHET_OF())){
2248

    
2249
                            for (Synonym synonymRelationOfParent:synonyMsOfParent){
2250

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

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

    
2263
                            if (doWithMisappliedNames){
2264

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

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

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

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

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

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

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

    
2303
                        for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2304
                            TaxonName synName;
2305
                            IZoologicalName inferredSynName;
2306

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

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

    
2315

    
2316
                        }
2317

    
2318
                        if (doWithMisappliedNames){
2319

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

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

    
2330

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

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

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

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

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

    
2360
                            synName = synonymRelationOfParent.getName();
2361

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

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

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

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

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

    
2384
                            //for all synonyms of the taxon
2385

    
2386
                            for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2387

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

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

    
2399
                            }
2400

    
2401

    
2402
                        }
2403

    
2404
                        if (doWithMisappliedNames){
2405

    
2406
                            for (TaxonRelationship parentRelationship: taxonRelListParent){
2407

    
2408
                                TaxonName misappliedParentName;
2409

    
2410
                                Taxon misappliedParent = parentRelationship.getFromTaxon();
2411
                                misappliedParentName = misappliedParent.getName();
2412

    
2413
                                HibernateProxyHelper.deproxy(misappliedParent);
2414

    
2415
                                // Set the sourceReference
2416
                                sourceReference = misappliedParent.getSec();
2417

    
2418
                                // Determine the idInSource
2419
                                String idInSourceParent = getIdInSource(misappliedParent);
2420

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

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

    
2433

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

    
2443

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

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

    
2481
        }
2482

    
2483
        return inferredSynonyms;
2484
    }
2485

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

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

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

    
2508
        String synTaxonInfraSpecificName= null;
2509

    
2510
        if (parentSynZooName.isSpecies()){
2511
            synTaxonInfraSpecificName = zooSynName.getInfraSpecificEpithet();
2512
        }
2513

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

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

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

    
2536

    
2537
        potentialCombination = Synonym.NewInstance(inferredSynName, null);
2538

    
2539
        // Set the sourceReference
2540
        potentialCombination.setSec(sourceReference);
2541

    
2542

    
2543
        // Determine the idInSource
2544
        String idInSourceSyn= getIdInSource(syn);
2545

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

    
2553
        return potentialCombination;
2554
    }
2555

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

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

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

    
2574
        //logger.warn(sourceReference.getTitleCache());
2575

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

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

    
2586

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

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

    
2600

    
2601
        inferredGenus = Synonym.NewInstance(inferredSynName, null);
2602

    
2603
        // Set the sourceReference
2604
        inferredGenus.setSec(sourceReference);
2605

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

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

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

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

    
2629
        taxon.addSynonym(inferredGenus, SynonymType.INFERRED_GENUS_OF());
2630

    
2631
        return inferredGenus;
2632
    }
2633

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

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

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

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

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

    
2662
        if (zooSynName.getInfraGenericEpithet() != null){
2663
            synInfraGenericEpithet = zooSynName.getInfraGenericEpithet();
2664
        }
2665

    
2666
        if (zooSynName.isInfraSpecific()){
2667
            synSpecificEpithet = zooSynName.getSpecificEpithet();
2668
        }
2669

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

    
2674
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2675

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

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

    
2692
        inferredEpithet = Synonym.NewInstance(inferredSynName, null);
2693

    
2694
        // Set the sourceReference
2695
        inferredEpithet.setSec(sourceReference);
2696

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

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

    
2710

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

    
2714
        inferredEpithet.addSource(originalSource);
2715

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

    
2719
        inferredSynName.addSource(originalSource);
2720

    
2721

    
2722

    
2723
        taxon.addSynonym(inferredEpithet, SynonymType.INFERRED_EPITHET_OF());
2724

    
2725
        return inferredEpithet;
2726
    }
2727

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

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

    
2769

    
2770
        return idInSource;
2771
    }
2772

    
2773

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

    
2790
        return citation;
2791
    }
2792

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

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

    
2801
        return inferredSynonyms;
2802
    }
2803

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

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

    
2812
        if (taxonBase == null) {
2813
            return list;
2814
        }
2815

    
2816
        taxonBase = load(taxonBase.getUuid());
2817

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

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

    
2844
        result.addUpdatedObject(fromTaxon);
2845
        result.addUpdatedObject(toTaxon);
2846
        result.addUpdatedObject(result.getCdmEntity());
2847

    
2848
        return result;
2849
    }
2850

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

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

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

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

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

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

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

    
2911
        return result;
2912
    }
2913

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

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

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

    
2945

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

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

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

    
2969
        return result;
2970
    }
2971

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

    
2976
        //preliminary implementation
2977

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

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

    
3000
        return result;
3001
    }
3002

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

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

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

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

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

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

    
3049

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
3172
		return result;
3173
	}
3174

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

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

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

    
3202
        }
3203

    
3204
    	return result;
3205
	}
3206

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

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

    
3219

    
3220
}
(94-94/101)