Project

General

Profile

Download (153 KB) Statistics
| Branch: | Tag: | Revision:
1
// $Id$
2
/**
3
* Copyright (C) 2007 EDIT
4
* European Distributed Institute of Taxonomy
5
* http://www.e-taxonomy.eu
6
*
7
* The contents of this file are subject to the Mozilla Public License Version 1.1
8
* See LICENSE.TXT at the top of this package for the full license terms.
9
*/
10

    
11
package eu.etaxonomy.cdm.api.service;
12

    
13
import java.io.IOException;
14
import java.util.ArrayList;
15
import java.util.EnumSet;
16
import java.util.HashMap;
17
import java.util.HashSet;
18
import java.util.Iterator;
19
import java.util.List;
20
import java.util.Map;
21
import java.util.Set;
22
import java.util.UUID;
23

    
24
import javax.persistence.EntityNotFoundException;
25

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

    
42
import eu.etaxonomy.cdm.api.service.config.DeleteConfiguratorBase;
43
import eu.etaxonomy.cdm.api.service.config.IFindTaxaAndNamesConfigurator;
44
import eu.etaxonomy.cdm.api.service.config.IncludedTaxonConfiguration;
45
import eu.etaxonomy.cdm.api.service.config.MatchingTaxonConfigurator;
46
import eu.etaxonomy.cdm.api.service.config.NodeDeletionConfigurator.ChildHandling;
47
import eu.etaxonomy.cdm.api.service.config.SynonymDeletionConfigurator;
48
import eu.etaxonomy.cdm.api.service.config.TaxonDeletionConfigurator;
49
import eu.etaxonomy.cdm.api.service.dto.FindByIdentifierDTO;
50
import eu.etaxonomy.cdm.api.service.dto.IncludedTaxaDTO;
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.LuceneSearch;
61
import eu.etaxonomy.cdm.api.service.search.QueryFactory;
62
import eu.etaxonomy.cdm.api.service.search.SearchResult;
63
import eu.etaxonomy.cdm.api.service.search.SearchResultBuilder;
64
import eu.etaxonomy.cdm.api.service.util.TaxonRelationshipEdge;
65
import eu.etaxonomy.cdm.common.monitor.IProgressMonitor;
66
import eu.etaxonomy.cdm.hibernate.HibernateProxyHelper;
67
import eu.etaxonomy.cdm.hibernate.search.DefinedTermBaseClassBridge;
68
import eu.etaxonomy.cdm.hibernate.search.GroupByTaxonClassBridge;
69
import eu.etaxonomy.cdm.hibernate.search.MultilanguageTextFieldBridge;
70
import eu.etaxonomy.cdm.model.CdmBaseType;
71
import eu.etaxonomy.cdm.model.common.Annotation;
72
import eu.etaxonomy.cdm.model.common.AnnotationType;
73
import eu.etaxonomy.cdm.model.common.CdmBase;
74
import eu.etaxonomy.cdm.model.common.DefinedTerm;
75
import eu.etaxonomy.cdm.model.common.IdentifiableEntity;
76
import eu.etaxonomy.cdm.model.common.IdentifiableSource;
77
import eu.etaxonomy.cdm.model.common.Language;
78
import eu.etaxonomy.cdm.model.common.OrderedTermVocabulary;
79
import eu.etaxonomy.cdm.model.common.OriginalSourceType;
80
import eu.etaxonomy.cdm.model.common.RelationshipBase;
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.NameRelationship;
100
import eu.etaxonomy.cdm.model.name.Rank;
101
import eu.etaxonomy.cdm.model.name.TaxonNameBase;
102
import eu.etaxonomy.cdm.model.name.ZoologicalName;
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.ITaxonTreeNode;
109
import eu.etaxonomy.cdm.model.taxon.Synonym;
110
import eu.etaxonomy.cdm.model.taxon.SynonymRelationship;
111
import eu.etaxonomy.cdm.model.taxon.SynonymRelationshipType;
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.fetch.CdmFetch;
127
import eu.etaxonomy.cdm.persistence.query.MatchMode;
128
import eu.etaxonomy.cdm.persistence.query.OrderHint;
129
import eu.etaxonomy.cdm.persistence.query.OrderHint.SortOrder;
130
import eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy;
131

    
132

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

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

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

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

    
149
    @Autowired
150
    private ITaxonNodeDao taxonNodeDao;
151

    
152
    @Autowired
153
    private ITaxonNameDao nameDao;
154

    
155
    @Autowired
156
    private INameService nameService;
157

    
158
    @Autowired
159
    private IOccurrenceService occurrenceService;
160

    
161
    @Autowired
162
    private ITaxonNodeService nodeService;
163

    
164
    @Autowired
165
    private ICdmGenericDao genericDao;
166

    
167
    @Autowired
168
    private IDescriptionService descriptionService;
169

    
170
    @Autowired
171
    private IOrderedTermVocabularyDao orderedVocabularyDao;
172

    
173
    @Autowired
174
    private IOccurrenceDao occurrenceDao;
175

    
176
    @Autowired
177
    private IClassificationDao classificationDao;
178

    
179
    @Autowired
180
    private AbstractBeanInitializer beanInitializer;
181

    
182
    @Autowired
183
    private ILuceneIndexToolProvider luceneIndexToolProvider;
184

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

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

    
201
    /**
202
     * FIXME Candidate for harmonization
203
     * merge with getRootTaxa(Reference sec, ..., ...)
204
     *  (non-Javadoc)
205
     * @see eu.etaxonomy.cdm.api.service.ITaxonService#getRootTaxa(eu.etaxonomy.cdm.model.reference.Reference, boolean)
206
     */
207
    @Override
208
    public List<Taxon> getRootTaxa(Reference sec, CdmFetch cdmFetch, boolean onlyWithChildren) {
209
        if (cdmFetch == null){
210
            cdmFetch = CdmFetch.NO_FETCH();
211
        }
212
        return dao.getRootTaxa(sec, cdmFetch, onlyWithChildren, false);
213
    }
214

    
215
    @Override
216
    public List<Taxon> getRootTaxa(Rank rank, Reference sec, boolean onlyWithChildren,boolean withMisapplications, List<String> propertyPaths) {
217
        return dao.getRootTaxa(rank, sec, null, onlyWithChildren, withMisapplications, propertyPaths);
218
    }
219

    
220
    @Override
221
    public List<RelationshipBase> getAllRelationships(int limit, int start){
222
        return dao.getAllRelationships(limit, start);
223
    }
224

    
225
    /**
226
     * FIXME Candidate for harmonization
227
     * is this the same as termService.getVocabulary(VocabularyEnum.TaxonRelationshipType) ?
228
     */
229
    @Override
230
    @Deprecated
231
    public OrderedTermVocabulary<TaxonRelationshipType> getTaxonRelationshipTypeVocabulary() {
232

    
233
        String taxonRelTypeVocabularyId = "15db0cf7-7afc-4a86-a7d4-221c73b0c9ac";
234
        UUID uuid = UUID.fromString(taxonRelTypeVocabularyId);
235
        OrderedTermVocabulary<TaxonRelationshipType> taxonRelTypeVocabulary =
236
            (OrderedTermVocabulary)orderedVocabularyDao.findByUuid(uuid);
237
        return taxonRelTypeVocabulary;
238
    }
239

    
240
    @Override
241
    @Transactional(readOnly = false)
242
    public UpdateResult swapSynonymAndAcceptedTaxon(Synonym synonym, Taxon acceptedTaxon){
243
    	UpdateResult result = new UpdateResult();
244
        TaxonNameBase<?,?> synonymName = synonym.getName();
245
        synonymName.removeTaxonBase(synonym);
246
        TaxonNameBase<?,?> taxonName = acceptedTaxon.getName();
247
        taxonName.removeTaxonBase(acceptedTaxon);
248

    
249
        synonym.setName(taxonName);
250
        acceptedTaxon.setName(synonymName);
251
        saveOrUpdate(synonym);
252
        saveOrUpdate(acceptedTaxon);
253
        result.addUpdatedObject(acceptedTaxon);
254
        result.addUpdatedObject(synonym);
255
		return result;
256

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

    
262

    
263
    @Override
264
    @Transactional(readOnly = false)
265
    public Taxon changeSynonymToAcceptedTaxon(Synonym synonym, Taxon acceptedTaxon, boolean deleteSynonym, boolean copyCitationInfo, Reference citation, String microCitation) throws HomotypicalGroupChangeException{
266

    
267
        TaxonNameBase<?,?> acceptedName = acceptedTaxon.getName();
268
        TaxonNameBase<?,?> synonymName = synonym.getName();
269
        HomotypicalGroup synonymHomotypicGroup = synonymName.getHomotypicalGroup();
270

    
271
        //check synonym is not homotypic
272
        if (acceptedName.getHomotypicalGroup().equals(synonymHomotypicGroup)){
273
            String message = "The accepted taxon and the synonym are part of the same homotypical group and therefore can not be both accepted.";
274
            throw new HomotypicalGroupChangeException(message);
275
        }
276

    
277
        Taxon newAcceptedTaxon = Taxon.NewInstance(synonymName, acceptedTaxon.getSec());
278
        dao.save(newAcceptedTaxon);
279
        SynonymRelationshipType relTypeForGroup = SynonymRelationshipType.HOMOTYPIC_SYNONYM_OF();
280
        List<Synonym> heteroSynonyms = acceptedTaxon.getSynonymsInGroup(synonymHomotypicGroup);
281
        Set<NameRelationship> basionymsAndReplacedSynonyms = synonymHomotypicGroup.getBasionymAndReplacedSynonymRelations();
282

    
283
        for (Synonym heteroSynonym : heteroSynonyms){
284
            if (synonym.equals(heteroSynonym)){
285
                acceptedTaxon.removeSynonym(heteroSynonym, false);
286

    
287
            }else{
288
                //move synonyms in same homotypic group to new accepted taxon
289
                heteroSynonym.replaceAcceptedTaxon(newAcceptedTaxon, relTypeForGroup, copyCitationInfo, citation, microCitation);
290
            }
291
        }
292
        dao.saveOrUpdate(acceptedTaxon);
293
        //synonym.getName().removeTaxonBase(synonym);
294

    
295
        if (deleteSynonym){
296
//			deleteSynonym(synonym, taxon, false);
297
            try {
298
                this.dao.flush();
299
                SynonymDeletionConfigurator config = new SynonymDeletionConfigurator();
300
                config.setDeleteNameIfPossible(false);
301
                this.deleteSynonym(synonym, acceptedTaxon, config);
302

    
303
            } catch (Exception e) {
304
                logger.info("Can't delete old synonym from database");
305
            }
306
        }
307

    
308
        return newAcceptedTaxon;
309
    }
310

    
311
    @Override
312
    @Transactional(readOnly = false)
313
    public UpdateResult changeSynonymToAcceptedTaxon(UUID synonymUuid,
314
            UUID acceptedTaxonUuid,
315
            UUID newParentNodeUuid,
316
            boolean deleteSynonym,
317
            boolean copyCitationInfo,
318
            Reference citation,
319
            String microCitation) throws HomotypicalGroupChangeException {
320
        UpdateResult result = new UpdateResult();
321
        Synonym synonym = CdmBase.deproxy(dao.load(synonymUuid), Synonym.class);
322
        Taxon acceptedTaxon = CdmBase.deproxy(dao.load(acceptedTaxonUuid), Taxon.class);
323
        Taxon taxon =  changeSynonymToAcceptedTaxon(synonym, acceptedTaxon, deleteSynonym, copyCitationInfo, citation, microCitation);
324
        TaxonNode newParentNode = taxonNodeDao.load(newParentNodeUuid);
325
        TaxonNode newNode = newParentNode.addChildTaxon(taxon, null, null);
326
        taxonNodeDao.save(newNode);
327
        result.addUpdatedObject(taxon);
328
        result.addUpdatedObject(acceptedTaxon);
329
        result.setCdmEntity(newNode);
330
        return result;
331
    }
332

    
333
    @Override
334
    @Transactional(readOnly = false)
335
    public UpdateResult changeSynonymToRelatedTaxon(UUID synonymUuid,
336
            UUID toTaxonUuid,
337
            TaxonRelationshipType taxonRelationshipType,
338
            Reference citation,
339
            String microcitation){
340

    
341
        UpdateResult result = new UpdateResult();
342
        Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
343
        Synonym synonym = (Synonym) dao.load(synonymUuid);
344
        Taxon relatedTaxon = changeSynonymToRelatedTaxon(synonym, toTaxon, taxonRelationshipType, citation, microcitation);
345
        result.setCdmEntity(relatedTaxon);
346
        result.addUpdatedObject(relatedTaxon);
347
        result.addUpdatedObject(toTaxon);
348
        return result;
349
    }
350

    
351
    @Override
352
    @Transactional(readOnly = false)
353
    public Taxon changeSynonymToRelatedTaxon(Synonym synonym, Taxon toTaxon, TaxonRelationshipType taxonRelationshipType, Reference citation, String microcitation){
354

    
355
        // Get name from synonym
356
        if (synonym == null){
357
            return null;
358
        }
359
        TaxonNameBase<?, ?> synonymName = synonym.getName();
360

    
361
      /*  // remove synonym from taxon
362
        toTaxon.removeSynonym(synonym);
363
*/
364
        // Create a taxon with synonym name
365
        Taxon fromTaxon = Taxon.NewInstance(synonymName, null);
366

    
367
        // Add taxon relation
368
        fromTaxon.addTaxonRelation(toTaxon, taxonRelationshipType, citation, microcitation);
369

    
370
        // since we are swapping names, we have to detach the name from the synonym completely.
371
        // Otherwise the synonym will still be in the list of typified names.
372
       // synonym.getName().removeTaxonBase(synonym);
373
        this.deleteSynonym(synonym, null);
374

    
375
        return fromTaxon;
376
    }
377

    
378
    @Transactional(readOnly = false)
379
    @Override
380
    public void changeHomotypicalGroupOfSynonym(Synonym synonym, HomotypicalGroup newHomotypicalGroup, Taxon targetTaxon,
381
                        boolean removeFromOtherTaxa, boolean setBasionymRelationIfApplicable){
382
        // Get synonym name
383
        TaxonNameBase synonymName = synonym.getName();
384
        HomotypicalGroup oldHomotypicalGroup = synonymName.getHomotypicalGroup();
385

    
386

    
387
        // Switch groups
388
        oldHomotypicalGroup.removeTypifiedName(synonymName, false);
389
        newHomotypicalGroup.addTypifiedName(synonymName);
390

    
391
        //remove existing basionym relationships
392
        synonymName.removeBasionyms();
393

    
394
        //add basionym relationship
395
        if (setBasionymRelationIfApplicable){
396
            Set<TaxonNameBase> basionyms = newHomotypicalGroup.getBasionyms();
397
            for (TaxonNameBase basionym : basionyms){
398
                synonymName.addBasionym(basionym);
399
            }
400
        }
401

    
402
        //set synonym relationship correctly
403
//			SynonymRelationship relToTaxon = null;
404
        boolean relToTargetTaxonExists = false;
405
        Set<SynonymRelationship> existingRelations = synonym.getSynonymRelations();
406
        for (SynonymRelationship rel : existingRelations){
407
            Taxon acceptedTaxon = rel.getAcceptedTaxon();
408
            boolean isTargetTaxon = acceptedTaxon != null && acceptedTaxon.equals(targetTaxon);
409
            HomotypicalGroup acceptedGroup = acceptedTaxon.getHomotypicGroup();
410
            boolean isHomotypicToTaxon = acceptedGroup.equals(newHomotypicalGroup);
411
            SynonymRelationshipType newRelationType = isHomotypicToTaxon? SynonymRelationshipType.HOMOTYPIC_SYNONYM_OF() : SynonymRelationshipType.HETEROTYPIC_SYNONYM_OF();
412
            rel.setType(newRelationType);
413
            //TODO handle citation and microCitation
414

    
415
            if (isTargetTaxon){
416
                relToTargetTaxonExists = true;
417
            }else{
418
                if (removeFromOtherTaxa){
419
                    acceptedTaxon.removeSynonym(synonym, false);
420
                }else{
421
                    //do nothing
422
                }
423
            }
424
        }
425
        if (targetTaxon != null &&  ! relToTargetTaxonExists ){
426
            Taxon acceptedTaxon = targetTaxon;
427
            HomotypicalGroup acceptedGroup = acceptedTaxon.getHomotypicGroup();
428
            boolean isHomotypicToTaxon = acceptedGroup.equals(newHomotypicalGroup);
429
            SynonymRelationshipType relType = isHomotypicToTaxon? SynonymRelationshipType.HOMOTYPIC_SYNONYM_OF() : SynonymRelationshipType.HETEROTYPIC_SYNONYM_OF();
430
            //TODO handle citation and microCitation
431
            Reference citation = null;
432
            String microCitation = null;
433
            acceptedTaxon.addSynonym(synonym, relType, citation, microCitation);
434
        }
435

    
436
    }
437

    
438
    @Override
439
    @Transactional(readOnly = false)
440
    public void updateTitleCache(Class<? extends TaxonBase> clazz, Integer stepSize, IIdentifiableEntityCacheStrategy<TaxonBase> cacheStrategy, IProgressMonitor monitor) {
441
        if (clazz == null){
442
            clazz = TaxonBase.class;
443
        }
444
        super.updateTitleCacheImpl(clazz, stepSize, cacheStrategy, monitor);
445
    }
446

    
447
    @Override
448
    @Autowired
449
    protected void setDao(ITaxonDao dao) {
450
        this.dao = dao;
451
    }
452

    
453
    @Override
454
    public Pager<TaxonBase> findTaxaByName(Class<? extends TaxonBase> clazz, String uninomial,	String infragenericEpithet, String specificEpithet,	String infraspecificEpithet, Rank rank, Integer pageSize,Integer pageNumber) {
455
        Integer numberOfResults = dao.countTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, rank);
456

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

    
462
        return new DefaultPagerImpl<TaxonBase>(pageNumber, numberOfResults, pageSize, results);
463
    }
464

    
465
    @Override
466
    public List<TaxonBase> listTaxaByName(Class<? extends TaxonBase> clazz, String uninomial,	String infragenericEpithet, String specificEpithet,	String infraspecificEpithet, Rank rank, Integer pageSize,Integer pageNumber) {
467
        Integer numberOfResults = dao.countTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, rank);
468

    
469
        List<TaxonBase> results = new ArrayList<TaxonBase>();
470
        if(numberOfResults > 0) { // no point checking again
471
            results = dao.findTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, rank, pageSize, pageNumber);
472
        }
473

    
474
        return results;
475
    }
476

    
477
    @Override
478
    public List<TaxonRelationship> listToTaxonRelationships(Taxon taxon, TaxonRelationshipType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths){
479
        Integer numberOfResults = dao.countTaxonRelationships(taxon, type, TaxonRelationship.Direction.relatedTo);
480

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

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

    
492
        List<TaxonRelationship> results = new ArrayList<TaxonRelationship>();
493
        if(numberOfResults > 0) { // no point checking again
494
            results = dao.getTaxonRelationships(taxon, type, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedTo);
495
        }
496
        return new DefaultPagerImpl<TaxonRelationship>(pageNumber, numberOfResults, pageSize, results);
497
    }
498

    
499
    @Override
500
    public List<TaxonRelationship> listFromTaxonRelationships(Taxon taxon, TaxonRelationshipType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths){
501
        Integer numberOfResults = dao.countTaxonRelationships(taxon, type, TaxonRelationship.Direction.relatedFrom);
502

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

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

    
514
        List<TaxonRelationship> results = new ArrayList<TaxonRelationship>();
515
        if(numberOfResults > 0) { // no point checking again
516
            results = dao.getTaxonRelationships(taxon, type, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedFrom);
517
        }
518
        return new DefaultPagerImpl<TaxonRelationship>(pageNumber, numberOfResults, pageSize, results);
519
    }
520

    
521
    @Override
522
    public List<Taxon> listAcceptedTaxaFor(UUID synonymUuid, UUID classificationUuid, Integer pageSize, Integer pageNumber,
523
            List<OrderHint> orderHints, List<String> propertyPaths){
524
        return pageAcceptedTaxaFor(synonymUuid, classificationUuid, pageSize, pageNumber, orderHints, propertyPaths).getRecords();
525
    }
526

    
527
    @Override
528
    public Pager<Taxon> pageAcceptedTaxaFor(UUID synonymUuid, UUID classificationUuid, Integer pageSize, Integer pageNumber,
529
            List<OrderHint> orderHints, List<String> propertyPaths){
530

    
531
        List<Taxon> list = new ArrayList<Taxon>();
532
        Long count = 0l;
533

    
534
        Synonym synonym = null;
535

    
536
        try {
537
            synonym = (Synonym) dao.load(synonymUuid);
538
        } catch (ClassCastException e){
539
            throw new EntityNotFoundException("The TaxonBase entity referenced by " + synonymUuid + " is not a Synonmy");
540
        } catch (NullPointerException e){
541
            throw new EntityNotFoundException("No TaxonBase entity found for " + synonymUuid);
542
        }
543

    
544
        Classification classificationFilter = null;
545
        if(classificationUuid != null){
546
            try {
547
            classificationFilter = classificationDao.load(classificationUuid);
548
            } catch (NullPointerException e){
549
                throw new EntityNotFoundException("No Classification entity found for " + classificationUuid);
550
            }
551
            if(classificationFilter == null){
552

    
553
            }
554
        }
555

    
556
        count = dao.countAcceptedTaxaFor(synonym, classificationFilter) ;
557
        if(count > (pageSize * pageNumber)){
558
            list = dao.listAcceptedTaxaFor(synonym, classificationFilter, pageSize, pageNumber, orderHints, propertyPaths);
559
        }
560

    
561
        return new DefaultPagerImpl<Taxon>(pageNumber, count.intValue(), pageSize, list);
562
    }
563

    
564

    
565
    @Override
566
    public Set<Taxon> listRelatedTaxa(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, Integer maxDepth,
567
            Integer limit, Integer start, List<String> propertyPaths) {
568

    
569
        Set<Taxon> relatedTaxa = collectRelatedTaxa(taxon, includeRelationships, new HashSet<Taxon>(), maxDepth);
570
        relatedTaxa.remove(taxon);
571
        beanInitializer.initializeAll(relatedTaxa, propertyPaths);
572
        return relatedTaxa;
573
    }
574

    
575

    
576
    /**
577
     * recursively collect related taxa for the given <code>taxon</code> . The returned list will also include the
578
     *  <code>taxon</code> supplied as parameter.
579
     *
580
     * @param taxon
581
     * @param includeRelationships
582
     * @param taxa
583
     * @param maxDepth can be <code>null</code> for infinite depth
584
     * @return
585
     */
586
    private Set<Taxon> collectRelatedTaxa(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, Set<Taxon> taxa, Integer maxDepth) {
587

    
588
        if(taxa.isEmpty()) {
589
            taxa.add(taxon);
590
        }
591

    
592
        if(includeRelationships.isEmpty()){
593
            return taxa;
594
        }
595

    
596
        if(maxDepth != null) {
597
            maxDepth--;
598
        }
599
        if(logger.isDebugEnabled()){
600
            logger.debug("collecting related taxa for " + taxon + " with maxDepth=" + maxDepth);
601
        }
602
        List<TaxonRelationship> taxonRelationships = dao.getTaxonRelationships(taxon, null, null, null, null, null, null);
603
        for (TaxonRelationship taxRel : taxonRelationships) {
604

    
605
            // skip invalid data
606
            if (taxRel.getToTaxon() == null || taxRel.getFromTaxon() == null || taxRel.getType() == null) {
607
                continue;
608
            }
609
            // filter by includeRelationships
610
            for (TaxonRelationshipEdge relationshipEdgeFilter : includeRelationships) {
611
                if ( relationshipEdgeFilter.getTaxonRelationshipType().equals(taxRel.getType()) ) {
612
                    if (relationshipEdgeFilter.getDirections().contains(Direction.relatedTo) && !taxa.contains(taxRel.getToTaxon())) {
613
                        if(logger.isDebugEnabled()){
614
                            logger.debug(maxDepth + ": " + taxon.getTitleCache() + " --[" + taxRel.getType().getLabel() + "]--> " + taxRel.getToTaxon().getTitleCache());
615
                        }
616
                        taxa.add(taxRel.getToTaxon());
617
                        if(maxDepth == null || maxDepth > 0) {
618
                            taxa.addAll(collectRelatedTaxa(taxRel.getToTaxon(), includeRelationships, taxa, maxDepth));
619
                        }
620
                    }
621
                    if(relationshipEdgeFilter.getDirections().contains(Direction.relatedFrom) && !taxa.contains(taxRel.getFromTaxon())) {
622
                        taxa.add(taxRel.getFromTaxon());
623
                        if(logger.isDebugEnabled()){
624
                            logger.debug(maxDepth + ": " +taxRel.getFromTaxon().getTitleCache() + " --[" + taxRel.getType().getLabel() + "]--> " + taxon.getTitleCache() );
625
                        }
626
                        if(maxDepth == null || maxDepth > 0) {
627
                            taxa.addAll(collectRelatedTaxa(taxRel.getFromTaxon(), includeRelationships, taxa, maxDepth));
628
                        }
629
                    }
630
                }
631
            }
632
        }
633
        return taxa;
634
    }
635

    
636
    @Override
637
    public Pager<SynonymRelationship> getSynonyms(Taxon taxon,	SynonymRelationshipType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
638
        Integer numberOfResults = dao.countSynonyms(taxon, type);
639

    
640
        List<SynonymRelationship> results = new ArrayList<SynonymRelationship>();
641
        if(numberOfResults > 0) { // no point checking again
642
            results = dao.getSynonyms(taxon, type, pageSize, pageNumber, orderHints, propertyPaths);
643
        }
644

    
645
        return new DefaultPagerImpl<SynonymRelationship>(pageNumber, numberOfResults, pageSize, results);
646
    }
647

    
648
    @Override
649
    public Pager<SynonymRelationship> getSynonyms(Synonym synonym,	SynonymRelationshipType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
650
        Integer numberOfResults = dao.countSynonyms(synonym, type);
651

    
652
        List<SynonymRelationship> results = new ArrayList<SynonymRelationship>();
653
        if(numberOfResults > 0) { // no point checking again
654
            results = dao.getSynonyms(synonym, type, pageSize, pageNumber, orderHints, propertyPaths);
655
        }
656

    
657
        return new DefaultPagerImpl<SynonymRelationship>(pageNumber, numberOfResults, pageSize, results);
658
    }
659

    
660
    @Override
661
    public List<List<Synonym>> getSynonymsByHomotypicGroup(Taxon taxon, List<String> propertyPaths){
662
         List<List<Synonym>> result = new ArrayList<List<Synonym>>();
663
        Taxon t = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
664

    
665
        //homotypic
666
        result.add(t.getHomotypicSynonymsByHomotypicGroup());
667

    
668
        //heterotypic
669
        List<HomotypicalGroup> homotypicalGroups = t.getHeterotypicSynonymyGroups();
670
        for(HomotypicalGroup homotypicalGroup : homotypicalGroups){
671
            result.add(t.getSynonymsInGroup(homotypicalGroup));
672
        }
673

    
674
        return result;
675

    
676
    }
677

    
678
    @Override
679
    public List<Synonym> getHomotypicSynonymsByHomotypicGroup(Taxon taxon, List<String> propertyPaths){
680
        Taxon t = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
681
        return t.getHomotypicSynonymsByHomotypicGroup();
682
    }
683

    
684
    @Override
685
    public List<List<Synonym>> getHeterotypicSynonymyGroups(Taxon taxon, List<String> propertyPaths){
686
        Taxon t = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
687
        List<HomotypicalGroup> homotypicalGroups = t.getHeterotypicSynonymyGroups();
688
        List<List<Synonym>> heterotypicSynonymyGroups = new ArrayList<List<Synonym>>(homotypicalGroups.size());
689
        for(HomotypicalGroup homotypicalGroup : homotypicalGroups){
690
            heterotypicSynonymyGroups.add(t.getSynonymsInGroup(homotypicalGroup));
691
        }
692
        return heterotypicSynonymyGroups;
693
    }
694

    
695
    @Override
696
    public List<UuidAndTitleCache<IdentifiableEntity>> findTaxaAndNamesForEditor(IFindTaxaAndNamesConfigurator configurator){
697

    
698
        List<UuidAndTitleCache<IdentifiableEntity>> results = new ArrayList<UuidAndTitleCache<IdentifiableEntity>>();
699

    
700

    
701
        if (configurator.isDoSynonyms() || configurator.isDoTaxa() || configurator.isDoNamesWithoutTaxa()){
702
        	results = dao.getTaxaByNameForEditor(configurator.isDoTaxa(), configurator.isDoSynonyms(), configurator.isDoNamesWithoutTaxa(), configurator.isDoMisappliedNames(),configurator.getTitleSearchStringSqlized(), configurator.getClassification(), configurator.getMatchMode(), configurator.getNamedAreas());
703
        }
704
        if (configurator.isDoTaxaByCommonNames()) {
705
            //if(configurator.getPageSize() == null ){
706
                List<UuidAndTitleCache<IdentifiableEntity>> commonNameResults = dao.getTaxaByCommonNameForEditor(configurator.getTitleSearchStringSqlized(), configurator.getClassification(), configurator.getMatchMode(), configurator.getNamedAreas());
707
                if(commonNameResults != null){
708
                    results.addAll(commonNameResults);
709
                }
710
           // }
711
        }
712
        return results;
713
    }
714

    
715
    @Override
716
    public Pager<IdentifiableEntity> findTaxaAndNames(IFindTaxaAndNamesConfigurator configurator) {
717

    
718
        List<IdentifiableEntity> results = new ArrayList<IdentifiableEntity>();
719
        int numberOfResults = 0; // overall number of results (as opposed to number of results per page)
720
        List<TaxonBase> taxa = null;
721

    
722
        // Taxa and synonyms
723
        long numberTaxaResults = 0L;
724

    
725

    
726
        List<String> propertyPath = new ArrayList<String>();
727
        if(configurator.getTaxonPropertyPath() != null){
728
            propertyPath.addAll(configurator.getTaxonPropertyPath());
729
        }
730

    
731

    
732
       if (configurator.isDoMisappliedNames() || configurator.isDoSynonyms() || configurator.isDoTaxa()){
733
            if(configurator.getPageSize() != null){ // no point counting if we need all anyway
734
                numberTaxaResults =
735
                    dao.countTaxaByName(configurator.isDoTaxa(),configurator.isDoSynonyms(), configurator.isDoMisappliedNames(),
736
                        configurator.getTitleSearchStringSqlized(), configurator.getClassification(), configurator.getMatchMode(),
737
                        configurator.getNamedAreas());
738
            }
739

    
740
            if(configurator.getPageSize() == null || numberTaxaResults > configurator.getPageSize() * configurator.getPageNumber()){ // no point checking again if less results
741
                taxa = dao.getTaxaByName(configurator.isDoTaxa(), configurator.isDoSynonyms(),
742
                    configurator.isDoMisappliedNames(), configurator.getTitleSearchStringSqlized(), configurator.getClassification(),
743
                    configurator.getMatchMode(), configurator.getNamedAreas(),
744
                    configurator.getPageSize(), configurator.getPageNumber(), propertyPath);
745
            }
746
       }
747

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

    
750
        if(taxa != null){
751
            results.addAll(taxa);
752
        }
753

    
754
        numberOfResults += numberTaxaResults;
755

    
756
        // Names without taxa
757
        if (configurator.isDoNamesWithoutTaxa()) {
758
            int numberNameResults = 0;
759

    
760
            List<? extends TaxonNameBase<?,?>> names =
761
                nameDao.findByName(configurator.getTitleSearchStringSqlized(), configurator.getMatchMode(),
762
                        configurator.getPageSize(), configurator.getPageNumber(), null, configurator.getTaxonNamePropertyPath());
763
            if (logger.isDebugEnabled()) { logger.debug(names.size() + " matching name(s) found"); }
764
            if (names.size() > 0) {
765
                for (TaxonNameBase<?,?> taxonName : names) {
766
                    if (taxonName.getTaxonBases().size() == 0) {
767
                        results.add(taxonName);
768
                        numberNameResults++;
769
                    }
770
                }
771
                if (logger.isDebugEnabled()) { logger.debug(numberNameResults + " matching name(s) without taxa found"); }
772
                numberOfResults += numberNameResults;
773
            }
774
        }
775

    
776
        // Taxa from common names
777

    
778
        if (configurator.isDoTaxaByCommonNames()) {
779
            taxa = new ArrayList<TaxonBase>();
780
            numberTaxaResults = 0;
781
            if(configurator.getPageSize() != null){// no point counting if we need all anyway
782
                numberTaxaResults = dao.countTaxaByCommonName(configurator.getTitleSearchStringSqlized(), configurator.getClassification(), configurator.getMatchMode(), configurator.getNamedAreas());
783
            }
784
            if(configurator.getPageSize() == null || numberTaxaResults > configurator.getPageSize() * configurator.getPageNumber()){
785
                List<Taxon> commonNameResults = dao.getTaxaByCommonName(configurator.getTitleSearchStringSqlized(), configurator.getClassification(), configurator.getMatchMode(), configurator.getNamedAreas(), configurator.getPageSize(), configurator.getPageNumber(), configurator.getTaxonPropertyPath());
786
                taxa.addAll(commonNameResults);
787
            }
788
            if(taxa != null){
789
                results.addAll(taxa);
790
            }
791
            numberOfResults += numberTaxaResults;
792

    
793
        }
794

    
795
       return new DefaultPagerImpl<IdentifiableEntity>
796
            (configurator.getPageNumber(), numberOfResults, configurator.getPageSize(), results);
797
    }
798

    
799
    public List<UuidAndTitleCache<TaxonBase>> getTaxonUuidAndTitleCache(){
800
        return dao.getUuidAndTitleCache();
801
    }
802

    
803
    @Override
804
    public List<MediaRepresentation> getAllMedia(Taxon taxon, int size, int height, int widthOrDuration, String[] mimeTypes){
805
        List<MediaRepresentation> medRep = new ArrayList<MediaRepresentation>();
806
        taxon = (Taxon)dao.load(taxon.getUuid());
807
        Set<TaxonDescription> descriptions = taxon.getDescriptions();
808
        for (TaxonDescription taxDesc: descriptions){
809
            Set<DescriptionElementBase> elements = taxDesc.getElements();
810
            for (DescriptionElementBase descElem: elements){
811
                for(Media media : descElem.getMedia()){
812

    
813
                    //find the best matching representation
814
                    medRep.add(MediaUtils.findBestMatchingRepresentation(media, null, size, height, widthOrDuration, mimeTypes));
815

    
816
                }
817
            }
818
        }
819
        return medRep;
820
    }
821

    
822
    @Override
823
    public List<Media> listTaxonDescriptionMedia(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, boolean limitToGalleries, List<String> propertyPath){
824
        return listMedia(taxon, includeRelationships, limitToGalleries, true, false, false, propertyPath);
825
    }
826

    
827
    @Override
828
    public List<Media> listMedia(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships,
829
            Boolean limitToGalleries, Boolean includeTaxonDescriptions, Boolean includeOccurrences,
830
            Boolean includeTaxonNameDescriptions, List<String> propertyPath) {
831

    
832
    //    logger.setLevel(Level.TRACE);
833
//        Logger.getLogger("org.hibernate.SQL").setLevel(Level.TRACE);
834

    
835
        logger.trace("listMedia() - START");
836

    
837
        Set<Taxon> taxa = new HashSet<Taxon>();
838
        List<Media> taxonMedia = new ArrayList<Media>();
839
        List<Media> nonImageGalleryImages = new ArrayList<Media>();
840

    
841
        if (limitToGalleries == null) {
842
            limitToGalleries = false;
843
        }
844

    
845
        // --- resolve related taxa
846
        if (includeRelationships != null && ! includeRelationships.isEmpty()) {
847
            logger.trace("listMedia() - resolve related taxa");
848
            taxa = listRelatedTaxa(taxon, includeRelationships, null, null, null, null);
849
        }
850

    
851
        taxa.add((Taxon) dao.load(taxon.getUuid()));
852

    
853
        if(includeTaxonDescriptions != null && includeTaxonDescriptions){
854
            logger.trace("listMedia() - includeTaxonDescriptions");
855
            List<TaxonDescription> taxonDescriptions = new ArrayList<TaxonDescription>();
856
            // --- TaxonDescriptions
857
            for (Taxon t : taxa) {
858
                taxonDescriptions.addAll(descriptionService.listTaxonDescriptions(t, null, null, null, null, propertyPath));
859
            }
860
            for (TaxonDescription taxonDescription : taxonDescriptions) {
861
                if (!limitToGalleries || taxonDescription.isImageGallery()) {
862
                    for (DescriptionElementBase element : taxonDescription.getElements()) {
863
                        for (Media media : element.getMedia()) {
864
                            if(taxonDescription.isImageGallery()){
865
                                taxonMedia.add(media);
866
                            }
867
                            else{
868
                                nonImageGalleryImages.add(media);
869
                            }
870
                        }
871
                    }
872
                }
873
            }
874
            //put images from image gallery first (#3242)
875
            taxonMedia.addAll(nonImageGalleryImages);
876
        }
877

    
878

    
879
        if(includeOccurrences != null && includeOccurrences) {
880
            logger.trace("listMedia() - includeOccurrences");
881
            Set<SpecimenOrObservationBase> specimensOrObservations = new HashSet<SpecimenOrObservationBase>();
882
            // --- Specimens
883
            for (Taxon t : taxa) {
884
                specimensOrObservations.addAll(occurrenceDao.listByAssociatedTaxon(null, t, null, null, null, null));
885
            }
886
            for (SpecimenOrObservationBase occurrence : specimensOrObservations) {
887

    
888
//            	direct media removed from specimen #3597
889
//              taxonMedia.addAll(occurrence.getMedia());
890

    
891
                // SpecimenDescriptions
892
                Set<SpecimenDescription> specimenDescriptions = occurrence.getSpecimenDescriptions();
893
                for (DescriptionBase specimenDescription : specimenDescriptions) {
894
                    if (!limitToGalleries || specimenDescription.isImageGallery()) {
895
                        Set<DescriptionElementBase> elements = specimenDescription.getElements();
896
                        for (DescriptionElementBase element : elements) {
897
                            for (Media media : element.getMedia()) {
898
                                taxonMedia.add(media);
899
                            }
900
                        }
901
                    }
902
                }
903

    
904
                if (occurrence.isInstanceOf(DerivedUnit.class)) {
905
                    DerivedUnit derivedUnit = CdmBase.deproxy(occurrence, DerivedUnit.class);
906
                    // Collection
907
                    //TODO why may collections have media attached? #
908
                    if (derivedUnit.getCollection() != null){
909
                        taxonMedia.addAll(derivedUnit.getCollection().getMedia());
910
                    }
911
                }
912
                //media in hierarchy
913
                taxonMedia.addAll(occurrenceService.getMediainHierarchy(occurrence, null, null, propertyPath).getRecords());
914
            }
915
        }
916

    
917
        if(includeTaxonNameDescriptions != null && includeTaxonNameDescriptions) {
918
            logger.trace("listMedia() - includeTaxonNameDescriptions");
919
            // --- TaxonNameDescription
920
            Set<TaxonNameDescription> nameDescriptions = new HashSet<TaxonNameDescription>();
921
            for (Taxon t : taxa) {
922
                nameDescriptions .addAll(t.getName().getDescriptions());
923
            }
924
            for(TaxonNameDescription nameDescription: nameDescriptions){
925
                if (!limitToGalleries || nameDescription.isImageGallery()) {
926
                    Set<DescriptionElementBase> elements = nameDescription.getElements();
927
                    for (DescriptionElementBase element : elements) {
928
                        for (Media media : element.getMedia()) {
929
                            taxonMedia.add(media);
930
                        }
931
                    }
932
                }
933
            }
934
        }
935

    
936

    
937
        logger.trace("listMedia() - initialize");
938
        beanInitializer.initializeAll(taxonMedia, propertyPath);
939

    
940
        logger.trace("listMedia() - END");
941

    
942
        return taxonMedia;
943
    }
944

    
945
    @Override
946
    public List<TaxonBase> findTaxaByID(Set<Integer> listOfIDs) {
947
        return this.dao.listByIds(listOfIDs, null, null, null, null);
948
    }
949

    
950
    @Override
951
    public TaxonBase findTaxonByUuid(UUID uuid, List<String> propertyPaths){
952
        return this.dao.findByUuid(uuid, null ,propertyPaths);
953
    }
954

    
955
    @Override
956
    public int countAllRelationships() {
957
        return this.dao.countAllRelationships();
958
    }
959

    
960
    @Override
961
    public List<TaxonNameBase> findIdenticalTaxonNames(List<String> propertyPath) {
962
        return this.dao.findIdenticalTaxonNames(propertyPath);
963
    }
964

    
965
    @Override
966
    @Transactional(readOnly = false)
967
    public DeleteResult deleteTaxon(UUID taxonUUID, TaxonDeletionConfigurator config, UUID classificationUuid)  {
968

    
969
    	if (config == null){
970
            config = new TaxonDeletionConfigurator();
971
        }
972
    	Taxon taxon = (Taxon)dao.load(taxonUUID);
973
    	DeleteResult result = new DeleteResult();
974
    	if (taxon == null){
975
    	    result.setAbort();
976
    	    result.addException(new Exception ("The taxon was already deleted."));
977
    	    return result;
978
    	}
979
    	taxon = HibernateProxyHelper.deproxy(taxon);
980
    	Classification classification = HibernateProxyHelper.deproxy(classificationDao.load(classificationUuid), Classification.class);
981
        result = isDeletable(taxon, config);
982

    
983
        if (result.isOk()){
984
            // --- DeleteSynonymRelations
985
            if (config.isDeleteSynonymRelations()){
986
                boolean removeSynonymNameFromHomotypicalGroup = false;
987
                // use tmp Set to avoid concurrent modification
988
                Set<SynonymRelationship> synRelsToDelete = new HashSet<SynonymRelationship>();
989
                synRelsToDelete.addAll(taxon.getSynonymRelations());
990
                for (SynonymRelationship synRel : synRelsToDelete){
991
                    Synonym synonym = synRel.getSynonym();
992
                    // taxon.removeSynonymRelation will set the accepted taxon and the synonym to NULL
993
                    // this will cause hibernate to delete the relationship since
994
                    // the SynonymRelationship field on both is annotated with removeOrphan
995
                    // so no further explicit deleting of the relationship should be done here
996
                    taxon.removeSynonymRelation(synRel, removeSynonymNameFromHomotypicalGroup);
997

    
998
                    // --- DeleteSynonymsIfPossible
999
                    if (config.isDeleteSynonymsIfPossible()){
1000
                        //TODO which value
1001
                        boolean newHomotypicGroupIfNeeded = true;
1002
                        SynonymDeletionConfigurator synConfig = new SynonymDeletionConfigurator();
1003
                        deleteSynonym(synonym, taxon, synConfig);
1004
                    }
1005
                    // relationship will be deleted by hibernate automatically,
1006
                    // see comment above and http://dev.e-taxonomy.eu/trac/ticket/3797
1007

    
1008
                }
1009
            }
1010

    
1011
            // --- DeleteTaxonRelationships
1012
            if (! config.isDeleteTaxonRelationships()){
1013
                if (taxon.getTaxonRelations().size() > 0){
1014
                    result.setAbort();
1015
                    result.addException(new Exception("Taxon can't be deleted as it is related to another taxon. " +
1016
                            "Remove taxon from all relations to other taxa prior to deletion."));
1017

    
1018
                }
1019
            } else{
1020
                TaxonDeletionConfigurator configRelTaxon = new TaxonDeletionConfigurator();
1021
                configRelTaxon.setDeleteTaxonNodes(false);
1022
                configRelTaxon.setDeleteConceptRelationships(true);
1023

    
1024
                for (TaxonRelationship taxRel: taxon.getTaxonRelations()){
1025
                    if (config.isDeleteMisappliedNamesAndInvalidDesignations()){
1026
                        if (taxRel.getType().equals(TaxonRelationshipType.MISAPPLIED_NAME_FOR()) || taxRel.getType().equals(TaxonRelationshipType.INVALID_DESIGNATION_FOR())){
1027
                            if (taxon.equals(taxRel.getToTaxon())){
1028
                                this.deleteTaxon(taxRel.getFromTaxon().getUuid(), config, classificationUuid);
1029
                            }
1030
                        }
1031
                    } else if (config.isDeleteConceptRelationships() && taxRel.getType().isConceptRelationship()){
1032

    
1033
                        if (taxon.equals(taxRel.getToTaxon()) && isDeletable(taxRel.getFromTaxon(), configRelTaxon).isOk()){
1034
                            this.deleteTaxon(taxRel.getFromTaxon().getUuid(), configRelTaxon, classificationUuid);
1035
                        }else if (isDeletable(taxRel.getToTaxon(), configRelTaxon).isOk()){
1036
                            this.deleteTaxon(taxRel.getToTaxon().getUuid(), configRelTaxon, classificationUuid);
1037
                        }
1038
                    }
1039
                    taxon.removeTaxonRelation(taxRel);
1040

    
1041
                }
1042
            }
1043

    
1044
            //    	TaxonDescription
1045
            if (config.isDeleteDescriptions()){
1046
                Set<TaxonDescription> descriptions = taxon.getDescriptions();
1047
                List<TaxonDescription> removeDescriptions = new ArrayList<TaxonDescription>();
1048
                for (TaxonDescription desc: descriptions){
1049
                    //TODO use description delete configurator ?
1050
                    //FIXME check if description is ALWAYS deletable
1051
                    if (desc.getDescribedSpecimenOrObservation() != null){
1052
                        result.setAbort();
1053
                        result.addException(new Exception("Taxon can't be deleted as it is used in a TaxonDescription" +
1054
                                " which also describes specimens or observations"));
1055
                        break;
1056
                    }
1057
                    removeDescriptions.add(desc);
1058

    
1059

    
1060
                }
1061
                if (result.isOk()){
1062
                    for (TaxonDescription desc: removeDescriptions){
1063
                        taxon.removeDescription(desc);
1064
                        descriptionService.delete(desc);
1065
                    }
1066
                } else {
1067
                    return result;
1068
                }
1069
            }
1070

    
1071

    
1072
         if (! config.isDeleteTaxonNodes() || (!config.isDeleteInAllClassifications() && classification == null && taxon.getTaxonNodes().size() > 1)){
1073
                //if (taxon.getTaxonNodes().size() > 0){
1074
                 result.addException(new Exception( "Taxon can't be deleted as it is used in more than one classification."));
1075
                   // throw new ReferencedObjectUndeletableException(message);
1076
                //}
1077
         }else{
1078
                if (taxon.getTaxonNodes().size() != 0){
1079
                    Set<TaxonNode> nodes = taxon.getTaxonNodes();
1080
                    Iterator<TaxonNode> iterator = nodes.iterator();
1081
                    TaxonNode node = null;
1082
                    boolean deleteChildren;
1083
                    if (config.getTaxonNodeConfig().getChildHandling().equals(ChildHandling.DELETE)){
1084
                        deleteChildren = true;
1085
                    }else {
1086
                        deleteChildren = false;
1087
                    }
1088
                    boolean success = true;
1089
                    if (!config.isDeleteInAllClassifications() && !(classification == null)){
1090
                        while (iterator.hasNext()){
1091
                            node = iterator.next();
1092
                            if (node.getClassification().equals(classification)){
1093
                                break;
1094
                            }
1095
                            node = null;
1096
                        }
1097
                        if (node != null){
1098
                            HibernateProxyHelper.deproxy(node, TaxonNode.class);
1099
                            success =taxon.removeTaxonNode(node, deleteChildren);
1100
                            nodeService.delete(node);
1101
                        } else {
1102
                        	result.setError();
1103
                        	result.addException(new Exception("The taxon can not be deleted because it is not used in defined classification."));
1104
                        }
1105
                    } else if (config.isDeleteInAllClassifications()){
1106
                        List<TaxonNode> nodesList = new ArrayList<TaxonNode>();
1107
                        nodesList.addAll(taxon.getTaxonNodes());
1108

    
1109
                            for (ITaxonTreeNode treeNode: nodesList){
1110
                                TaxonNode taxonNode = (TaxonNode) treeNode;
1111
                                if(!deleteChildren){
1112
                                    Object[] childNodes = taxonNode.getChildNodes().toArray();
1113
                                    for (Object childNode: childNodes){
1114
                                        TaxonNode childNodeCast = (TaxonNode) childNode;
1115
                                        taxonNode.getParent().addChildNode(childNodeCast, childNodeCast.getReference(), childNodeCast.getMicroReference());
1116
                                    }
1117

    
1118
                                    //taxon.removeTaxonNode(taxonNode);
1119
                                }
1120
                            }
1121
                        config.getTaxonNodeConfig().setDeleteElement(false);
1122
                        DeleteResult resultNodes = nodeService.deleteTaxonNodes(nodesList, config);
1123
                        if (!resultNodes.isOk()){
1124
                        	result.addExceptions(resultNodes.getExceptions());
1125
                        	result.setStatus(resultNodes.getStatus());
1126
                        } else {
1127
                            result.addUpdatedObjects(resultNodes.getUpdatedObjects());
1128
                        }
1129
                    }
1130
                    if (!success){
1131
                        result.setError();
1132
                        result.addException(new Exception("The taxon can not be deleted because the taxon node can not be removed."));
1133
                    }
1134
                }
1135
            }
1136

    
1137
            //TaxonNameBase
1138
            if (config.isDeleteNameIfPossible() && result.isOk()){
1139

    
1140

    
1141
                    //TaxonNameBase name = nameService.find(taxon.getName().getUuid());
1142
                    TaxonNameBase name = HibernateProxyHelper.deproxy(taxon.getName());
1143
                    //check whether taxon will be deleted or not
1144

    
1145
                    if ((taxon.getTaxonNodes() == null || taxon.getTaxonNodes().size()== 0) && name != null ){
1146

    
1147
                        //name.removeTaxonBase(taxon);
1148
                        //nameService.saveOrUpdate(name);
1149
                        taxon.setName(null);
1150
                        //dao.delete(taxon);
1151
                        DeleteResult nameResult = new DeleteResult();
1152

    
1153
                        //remove name if possible (and required)
1154
                        if (name != null && config.isDeleteNameIfPossible()){
1155
                        	nameResult = nameService.delete(name.getUuid(), config.getNameDeletionConfig());
1156
                        }
1157

    
1158
                        if (nameResult.isError() || nameResult.isAbort()){
1159
                        	//result.setError();
1160
                        	result.addRelatedObject(name);
1161
                        	result.addExceptions(nameResult.getExceptions());
1162
                        }
1163

    
1164
                    }
1165

    
1166
            }else {
1167
                taxon.setName(null);
1168
            }
1169

    
1170
            if ((taxon.getTaxonNodes() == null || taxon.getTaxonNodes().size()== 0)  && result.isOk()){
1171
            	try{
1172
            		UUID uuid = dao.delete(taxon);
1173

    
1174
            	}catch(Exception e){
1175
            		result.addException(e);
1176
            		result.setError();
1177

    
1178
            	}
1179
            } else {
1180
            	result.setError();
1181
            	result.addException(new Exception("The Taxon can't be deleted because it is used in a classification."));
1182

    
1183
            }
1184
        }
1185

    
1186
        return result;
1187

    
1188
    }
1189

    
1190
    private String checkForReferences(Taxon taxon){
1191
        Set<CdmBase> referencingObjects = genericDao.getReferencingObjects(taxon);
1192
        for (CdmBase referencingObject : referencingObjects){
1193
            //IIdentificationKeys (Media, Polytomous, MultiAccess)
1194
            if (HibernateProxyHelper.isInstanceOf(referencingObject, IIdentificationKey.class)){
1195
                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";
1196

    
1197
                return message;
1198
            }
1199

    
1200

    
1201
           /* //PolytomousKeyNode
1202
            if (referencingObject.isInstanceOf(PolytomousKeyNode.class)){
1203
                String message = "Taxon" + taxon.getTitleCache() + " can't be deleted as it is used in polytomous key node";
1204
                return message;
1205
            }*/
1206

    
1207
            //TaxonInteraction
1208
            if (referencingObject.isInstanceOf(TaxonInteraction.class)){
1209
                String message = "Taxon can't be deleted as it is used in taxonInteraction#taxon2";
1210
                return message;
1211
            }
1212

    
1213
          //TaxonInteraction
1214
            if (referencingObject.isInstanceOf(DeterminationEvent.class)){
1215
                String message = "Taxon can't be deleted as it is used in a determination event";
1216
                return message;
1217
            }
1218

    
1219
        }
1220

    
1221
        referencingObjects = null;
1222
        return null;
1223
    }
1224

    
1225
    private boolean checkForPolytomousKeys(Taxon taxon){
1226
        boolean result = false;
1227
        List<CdmBase> list = genericDao.getCdmBasesByFieldAndClass(PolytomousKeyNode.class, "taxon", taxon);
1228
        if (!list.isEmpty()) {
1229
            result = true;
1230
        }
1231
        return result;
1232
    }
1233

    
1234
    @Override
1235
    @Transactional(readOnly = false)
1236
    public DeleteResult delete(UUID synUUID){
1237
    	DeleteResult result = new DeleteResult();
1238
    	Synonym syn = (Synonym)dao.load(synUUID);
1239

    
1240
        return this.deleteSynonym(syn, null);
1241
    }
1242

    
1243

    
1244
    @Override
1245
    @Transactional(readOnly = false)
1246
    public DeleteResult deleteSynonym(Synonym synonym, SynonymDeletionConfigurator config) {
1247
        return deleteSynonym(synonym, null, config);
1248

    
1249
    }
1250

    
1251
    @Override
1252
    @Transactional(readOnly = false)
1253
    public DeleteResult deleteSynonym(UUID synonymUuid, SynonymDeletionConfigurator config) {
1254
        return deleteSynonym((Synonym)dao.load(synonymUuid), config);
1255

    
1256
    }
1257

    
1258
    @Transactional(readOnly = false)
1259
    @Override
1260
    public DeleteResult deleteSynonym(Synonym synonym, Taxon taxon, SynonymDeletionConfigurator config) {
1261
        DeleteResult result = new DeleteResult();
1262
    	if (synonym == null){
1263
    		result.setAbort();
1264
    		result.addException(new Exception("The synonym was already deleted."));
1265
    		return result;
1266
        }
1267

    
1268
        if (config == null){
1269
            config = new SynonymDeletionConfigurator();
1270
        }
1271

    
1272
        result = isDeletable(synonym, config);
1273

    
1274

    
1275
        if (result.isOk()){
1276

    
1277
            synonym = HibernateProxyHelper.deproxy(this.load(synonym.getUuid()), Synonym.class);
1278

    
1279
            //remove synonymRelationship
1280
            Set<Taxon> taxonSet = new HashSet<Taxon>();
1281
            if (taxon != null){
1282
                taxonSet.add(taxon);
1283
            }else{
1284
                taxonSet.addAll(synonym.getAcceptedTaxa());
1285
            }
1286
            for (Taxon relatedTaxon : taxonSet){
1287
            	relatedTaxon = HibernateProxyHelper.deproxy(relatedTaxon, Taxon.class);
1288
                relatedTaxon.removeSynonym(synonym, false);
1289
                this.saveOrUpdate(relatedTaxon);
1290
            }
1291
            this.saveOrUpdate(synonym);
1292

    
1293
            //TODO remove name from homotypical group?
1294

    
1295
            //remove synonym (if necessary)
1296

    
1297
            result.addUpdatedObject(taxon);
1298
            if (synonym.getSynonymRelations().isEmpty()){
1299
                TaxonNameBase<?,?> name = synonym.getName();
1300
                synonym.setName(null);
1301
                dao.delete(synonym);
1302

    
1303
                //remove name if possible (and required)
1304
                if (name != null && config.isDeleteNameIfPossible()){
1305

    
1306
                        DeleteResult nameDeleteresult = nameService.delete(name.getUuid(), config.getNameDeletionConfig());
1307
                        if (nameDeleteresult.isAbort()){
1308
                        	result.addExceptions(nameDeleteresult.getExceptions());
1309
                        	result.addRelatedObject(name);
1310
                        }
1311

    
1312
                }
1313

    
1314
            }else {
1315
            	result.setError();
1316
            	result.addException(new ReferencedObjectUndeletableException("Synonym can not be deleted it is used in an other synonymRelationship."));
1317
                return result;
1318
            }
1319

    
1320

    
1321
        }
1322
        return result;
1323
//        else{
1324
//        	List<Exception> exceptions = new ArrayList<Exception>();
1325
//        	for (String message :messages){
1326
//        		exceptions.add(new ReferencedObjectUndeletableException(message));
1327
//        	}
1328
//        	result.setError();
1329
//        	result.addExceptions(exceptions);
1330
//            return result;
1331
//        }
1332

    
1333

    
1334
    }
1335

    
1336
    @Override
1337
    public List<TaxonNameBase> findIdenticalTaxonNameIds(List<String> propertyPath) {
1338

    
1339
        return this.dao.findIdenticalNamesNew(propertyPath);
1340
    }
1341

    
1342
    @Override
1343
    public String getPhylumName(TaxonNameBase name){
1344
        return this.dao.getPhylumName(name);
1345
    }
1346

    
1347
    @Override
1348
    public long deleteSynonymRelationships(Synonym syn, Taxon taxon) {
1349
        return dao.deleteSynonymRelationships(syn, taxon);
1350
    }
1351

    
1352
    @Override
1353
    public long deleteSynonymRelationships(Synonym syn) {
1354
        return dao.deleteSynonymRelationships(syn, null);
1355
    }
1356

    
1357
    @Override
1358
    public List<SynonymRelationship> listSynonymRelationships(
1359
            TaxonBase taxonBase, SynonymRelationshipType type, Integer pageSize, Integer pageNumber,
1360
            List<OrderHint> orderHints, List<String> propertyPaths, Direction direction) {
1361
        Integer numberOfResults = dao.countSynonymRelationships(taxonBase, type, direction);
1362

    
1363
        List<SynonymRelationship> results = new ArrayList<SynonymRelationship>();
1364
        if(numberOfResults > 0) { // no point checking again
1365
            results = dao.getSynonymRelationships(taxonBase, type, pageSize, pageNumber, orderHints, propertyPaths, direction);
1366
        }
1367
        return results;
1368
    }
1369

    
1370
    @Override
1371
    public Taxon findBestMatchingTaxon(String taxonName) {
1372
        MatchingTaxonConfigurator config = MatchingTaxonConfigurator.NewInstance();
1373
        config.setTaxonNameTitle(taxonName);
1374
        return findBestMatchingTaxon(config);
1375
    }
1376

    
1377
    @Override
1378
    public Taxon findBestMatchingTaxon(MatchingTaxonConfigurator config) {
1379

    
1380
        Taxon bestCandidate = null;
1381
        try{
1382
            // 1. search for acceptet taxa
1383
            List<TaxonBase> taxonList = dao.findByNameTitleCache(true, false, config.getTaxonNameTitle(), null, MatchMode.EXACT, null, 0, null, null);
1384
            boolean bestCandidateMatchesSecUuid = false;
1385
            boolean bestCandidateIsInClassification = false;
1386
            int countEqualCandidates = 0;
1387
            for(TaxonBase taxonBaseCandidate : taxonList){
1388
                if(taxonBaseCandidate instanceof Taxon){
1389
                    Taxon newCanditate = CdmBase.deproxy(taxonBaseCandidate, Taxon.class);
1390
                    boolean newCandidateMatchesSecUuid = isMatchesSecUuid(newCanditate, config);
1391
                    if (! newCandidateMatchesSecUuid && config.isOnlyMatchingSecUuid() ){
1392
                        continue;
1393
                    }else if(newCandidateMatchesSecUuid && ! bestCandidateMatchesSecUuid){
1394
                        bestCandidate = newCanditate;
1395
                        countEqualCandidates = 1;
1396
                        bestCandidateMatchesSecUuid = true;
1397
                        continue;
1398
                    }
1399

    
1400
                    boolean newCandidateInClassification = isInClassification(newCanditate, config);
1401
                    if (! newCandidateInClassification && config.isOnlyMatchingClassificationUuid()){
1402
                        continue;
1403
                    }else if (newCandidateInClassification && ! bestCandidateIsInClassification){
1404
                        bestCandidate = newCanditate;
1405
                        countEqualCandidates = 1;
1406
                        bestCandidateIsInClassification = true;
1407
                        continue;
1408
                    }
1409
                    if (bestCandidate == null){
1410
                        bestCandidate = newCanditate;
1411
                        countEqualCandidates = 1;
1412
                        continue;
1413
                    }
1414

    
1415
                }else{  //not Taxon.class
1416
                    continue;
1417
                }
1418
                countEqualCandidates++;
1419

    
1420
            }
1421
            if (bestCandidate != null){
1422
                if(countEqualCandidates > 1){
1423
                    logger.info(countEqualCandidates + " equally matching TaxonBases found, using first accepted Taxon: " + bestCandidate.getTitleCache());
1424
                    return bestCandidate;
1425
                } else {
1426
                    logger.info("using accepted Taxon: " + bestCandidate.getTitleCache());
1427
                    return bestCandidate;
1428
                }
1429
            }
1430

    
1431

    
1432
            // 2. search for synonyms
1433
            if (config.isIncludeSynonyms()){
1434
                List<TaxonBase> synonymList = dao.findByNameTitleCache(false, true, config.getTaxonNameTitle(), null, MatchMode.EXACT, null, 0, null, null);
1435
                for(TaxonBase taxonBase : synonymList){
1436
                    if(taxonBase instanceof Synonym){
1437
                        Synonym synonym = CdmBase.deproxy(taxonBase, Synonym.class);
1438
                        Set<Taxon> acceptetdCandidates = synonym.getAcceptedTaxa();
1439
                        if(!acceptetdCandidates.isEmpty()){
1440
                            bestCandidate = acceptetdCandidates.iterator().next();
1441
                            if(acceptetdCandidates.size() == 1){
1442
                                logger.info(acceptetdCandidates.size() + " Accepted taxa found for synonym " + taxonBase.getTitleCache() + ", using first one: " + bestCandidate.getTitleCache());
1443
                                return bestCandidate;
1444
                            } else {
1445
                                logger.info("using accepted Taxon " +  bestCandidate.getTitleCache() + "for synonym " + taxonBase.getTitleCache());
1446
                                return bestCandidate;
1447
                            }
1448
                            //TODO extend method: search using treeUUID, using SecUUID, first find accepted then include synonyms until a matching taxon is found
1449
                        }
1450
                    }
1451
                }
1452
            }
1453

    
1454
        } catch (Exception e){
1455
            logger.error(e);
1456
            e.printStackTrace();
1457
        }
1458

    
1459
        return bestCandidate;
1460
    }
1461

    
1462
    private boolean isInClassification(Taxon taxon, MatchingTaxonConfigurator config) {
1463
        UUID configClassificationUuid = config.getClassificationUuid();
1464
        if (configClassificationUuid == null){
1465
            return false;
1466
        }
1467
        for (TaxonNode node : taxon.getTaxonNodes()){
1468
            UUID classUuid = node.getClassification().getUuid();
1469
            if (configClassificationUuid.equals(classUuid)){
1470
                return true;
1471
            }
1472
        }
1473
        return false;
1474
    }
1475

    
1476
    private boolean isMatchesSecUuid(Taxon taxon, MatchingTaxonConfigurator config) {
1477
        UUID configSecUuid = config.getSecUuid();
1478
        if (configSecUuid == null){
1479
            return false;
1480
        }
1481
        UUID taxonSecUuid = (taxon.getSec() == null)? null : taxon.getSec().getUuid();
1482
        return configSecUuid.equals(taxonSecUuid);
1483
    }
1484

    
1485
    @Override
1486
    public Synonym findBestMatchingSynonym(String taxonName) {
1487
        List<TaxonBase> synonymList = dao.findByNameTitleCache(false, true, taxonName, null, MatchMode.EXACT, null, 0, null, null);
1488
        if(! synonymList.isEmpty()){
1489
            Synonym result = CdmBase.deproxy(synonymList.iterator().next(), Synonym.class);
1490
            if(synonymList.size() == 1){
1491
                logger.info(synonymList.size() + " Synonym found " + result.getTitleCache() );
1492
                return result;
1493
            } else {
1494
                logger.info("Several matching synonyms found. Using first: " +  result.getTitleCache());
1495
                return result;
1496
            }
1497
        }
1498
        return null;
1499
    }
1500

    
1501
    @Override
1502
    @Transactional(readOnly = false)
1503
    public UpdateResult moveSynonymToAnotherTaxon(SynonymRelationship oldSynonymRelation,
1504
            Taxon newTaxon,
1505
            boolean moveHomotypicGroup,
1506
            SynonymRelationshipType newSynonymRelationshipType,
1507
            Reference reference,
1508
            String referenceDetail,
1509
            boolean keepReference) throws HomotypicalGroupChangeException {
1510

    
1511
        Synonym synonym = (Synonym) dao.load(oldSynonymRelation.getSynonym().getUuid());
1512
        Taxon fromTaxon = (Taxon) dao.load(oldSynonymRelation.getAcceptedTaxon().getUuid());
1513
        //TODO what if there is no name ?? Concepts may be cached (e.g. via TCS import)
1514
        TaxonNameBase<?,?> synonymName = synonym.getName();
1515
        TaxonNameBase<?,?> fromTaxonName = fromTaxon.getName();
1516
        //set default relationship type
1517
        if (newSynonymRelationshipType == null){
1518
            newSynonymRelationshipType = SynonymRelationshipType.HETEROTYPIC_SYNONYM_OF();
1519
        }
1520
        boolean newRelTypeIsHomotypic = newSynonymRelationshipType.equals(SynonymRelationshipType.HOMOTYPIC_SYNONYM_OF());
1521

    
1522
        HomotypicalGroup homotypicGroup = synonymName.getHomotypicalGroup();
1523
        int hgSize = homotypicGroup.getTypifiedNames().size();
1524
        boolean isSingleInGroup = !(hgSize > 1);
1525

    
1526
        if (! isSingleInGroup){
1527
            boolean isHomotypicToAccepted = synonymName.isHomotypic(fromTaxonName);
1528
            boolean hasHomotypicSynonymRelatives = isHomotypicToAccepted ? hgSize > 2 : hgSize > 1;
1529
            if (isHomotypicToAccepted){
1530
                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.";
1531
                String homotypicRelatives = hasHomotypicSynonymRelatives ? " and other synonym(s)":"";
1532
                message = String.format(message, homotypicRelatives);
1533
                throw new HomotypicalGroupChangeException(message);
1534
            }
1535
            if (! moveHomotypicGroup){
1536
                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.";
1537
                throw new HomotypicalGroupChangeException(message);
1538
            }
1539
        }else{
1540
            moveHomotypicGroup = true;  //single synonym always allows to moveCompleteGroup
1541
        }
1542
//        Assert.assertTrue("Synonym can only be moved with complete homotypic group", moveHomotypicGroup);
1543

    
1544
        UpdateResult result = new UpdateResult();
1545
        //move all synonyms to new taxon
1546
        List<Synonym> homotypicSynonyms = fromTaxon.getSynonymsInGroup(homotypicGroup);
1547
        for (Synonym syn: homotypicSynonyms){
1548
            Set<SynonymRelationship> synRelations = syn.getSynonymRelations();
1549
            for (SynonymRelationship synRelation : synRelations){
1550
                if (fromTaxon.equals(synRelation.getAcceptedTaxon())){
1551
                    Reference<?> newReference = reference;
1552
                    if (newReference == null && keepReference){
1553
                        newReference = synRelation.getCitation();
1554
                    }
1555
                    String newRefDetail = referenceDetail;
1556
                    if (newRefDetail == null && keepReference){
1557
                        newRefDetail = synRelation.getCitationMicroReference();
1558
                    }
1559
                    newTaxon = HibernateProxyHelper.deproxy(newTaxon, Taxon.class);
1560
                    fromTaxon = HibernateProxyHelper.deproxy(fromTaxon, Taxon.class);
1561
                    SynonymRelationship newSynRelation = newTaxon.addSynonym(syn, newSynonymRelationshipType, newReference, newRefDetail);
1562
                    fromTaxon.removeSynonymRelation(synRelation, false);
1563
//
1564
                    //change homotypic group of synonym if relType is 'homotypic'
1565
//                	if (newRelTypeIsHomotypic){
1566
//                		newTaxon.getName().getHomotypicalGroup().addTypifiedName(syn.getName());
1567
//                	}
1568
                    //set result
1569
                    if (!synRelation.equals(oldSynonymRelation)){
1570
                        result.setError();
1571
                    }
1572
                }
1573
            }
1574

    
1575
        }
1576
        result.addUpdatedObject(fromTaxon);
1577
        result.addUpdatedObject(newTaxon);
1578
        saveOrUpdate(fromTaxon);
1579
        saveOrUpdate(newTaxon);
1580
        //Assert that there is a result
1581
        if (result == null){
1582
            String message = "Old synonym relation could not be transformed into new relation. This should not happen.";
1583
            throw new IllegalStateException(message);
1584
        }
1585
        return result;
1586
    }
1587

    
1588
    @Override
1589
    public List<UuidAndTitleCache<TaxonBase>> getUuidAndTitleCacheTaxon() {
1590
        return dao.getUuidAndTitleCacheTaxon();
1591
    }
1592

    
1593
    @Override
1594
    public List<UuidAndTitleCache<TaxonBase>> getUuidAndTitleCacheSynonym() {
1595
        return dao.getUuidAndTitleCacheSynonym();
1596
    }
1597

    
1598
    @Override
1599
    public Pager<SearchResult<TaxonBase>> findByFullText(
1600
            Class<? extends TaxonBase> clazz, String queryString,
1601
            Classification classification, List<Language> languages,
1602
            boolean highlightFragments, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws CorruptIndexException, IOException, ParseException {
1603

    
1604

    
1605
        LuceneSearch luceneSearch = prepareFindByFullTextSearch(clazz, queryString, classification, languages, highlightFragments, null);
1606

    
1607
        // --- execute search
1608
        TopGroups<BytesRef> topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
1609

    
1610
        Map<CdmBaseType, String> idFieldMap = new HashMap<CdmBaseType, String>();
1611
        idFieldMap.put(CdmBaseType.TAXON, "id");
1612

    
1613
        // ---  initialize taxa, thighlight matches ....
1614
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1615
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1616
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1617

    
1618
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
1619
        return new DefaultPagerImpl<SearchResult<TaxonBase>>(pageNumber, totalHits, pageSize, searchResults);
1620
    }
1621

    
1622
    @Override
1623
    public Pager<SearchResult<TaxonBase>> findByDistribution(List<NamedArea> areaFilter, List<PresenceAbsenceTerm> statusFilter,
1624
            Classification classification,
1625
            Integer pageSize, Integer pageNumber,
1626
            List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, ParseException {
1627

    
1628
        LuceneSearch luceneSearch = prepareByDistributionSearch(areaFilter, statusFilter, classification);
1629

    
1630
        // --- execute search
1631
        TopGroups<BytesRef> topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
1632

    
1633
        Map<CdmBaseType, String> idFieldMap = new HashMap<CdmBaseType, String>();
1634
        idFieldMap.put(CdmBaseType.TAXON, "id");
1635

    
1636
        // ---  initialize taxa, thighlight matches ....
1637
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1638
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1639
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1640

    
1641
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
1642
        return new DefaultPagerImpl<SearchResult<TaxonBase>>(pageNumber, totalHits, pageSize, searchResults);
1643
    }
1644

    
1645
    /**
1646
     * @param clazz
1647
     * @param queryString
1648
     * @param classification
1649
     * @param languages
1650
     * @param highlightFragments
1651
     * @param sortFields TODO
1652
     * @param directorySelectClass
1653
     * @return
1654
     */
1655
    protected LuceneSearch prepareFindByFullTextSearch(Class<? extends CdmBase> clazz, String queryString, Classification classification, List<Language> languages,
1656
            boolean highlightFragments, SortField[] sortFields) {
1657
        Builder finalQueryBuilder = new Builder();
1658
        Builder textQueryBuilder = new Builder();
1659

    
1660
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1661
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1662

    
1663
        if(sortFields == null){
1664
            sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING,  false)};
1665
        }
1666
        luceneSearch.setSortFields(sortFields);
1667

    
1668
        // ---- search criteria
1669
        luceneSearch.setCdmTypRestriction(clazz);
1670

    
1671
        if(!queryString.isEmpty() && !queryString.equals("*") && !queryString.equals("?") ) {
1672
            textQueryBuilder.add(taxonBaseQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
1673
            textQueryBuilder.add(taxonBaseQueryFactory.newDefinedTermQuery("name.rank", queryString, languages), Occur.SHOULD);
1674
        }
1675

    
1676
        BooleanQuery textQuery = textQueryBuilder.build();
1677
        if(textQuery.clauses().size() > 0) {
1678
            finalQueryBuilder.add(textQuery, Occur.MUST);
1679
        }
1680

    
1681

    
1682
        if(classification != null){
1683
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery("taxonNodes.classification.id", classification), Occur.MUST);
1684
        }
1685
        luceneSearch.setQuery(finalQueryBuilder.build());
1686

    
1687
        if(highlightFragments){
1688
            luceneSearch.setHighlightFields(taxonBaseQueryFactory.getTextFieldNamesAsArray());
1689
        }
1690
        return luceneSearch;
1691
    }
1692

    
1693
    /**
1694
     * Uses org.apache.lucene.search.join.JoinUtil for query time joining, alternatively
1695
     * the BlockJoinQuery could be used. The latter might be more memory save but has the
1696
     * drawback of requiring to do the join an indexing time.
1697
     * see  http://dev.e-taxonomy.eu/trac/wiki/LuceneNotes#JoinsinLucene for more information on this.
1698
     *
1699
     * Joins TaxonRelationShip with Taxon depending on the direction of the given edge:
1700
     * <ul>
1701
     * <li>direct, everted: {@link Direction.relatedTo}: TaxonRelationShip.relatedTo.id --&gt; Taxon.id </li>
1702
     * <li>inverse: {@link Direction.relatedFrom}:  TaxonRelationShip.relatedFrom.id --&gt; Taxon.id </li>
1703
     * <ul>
1704
     * @param queryString
1705
     * @param classification
1706
     * @param languages
1707
     * @param highlightFragments
1708
     * @param sortFields TODO
1709
     *
1710
     * @return
1711
     * @throws IOException
1712
     */
1713
    protected LuceneSearch prepareFindByTaxonRelationFullTextSearch(TaxonRelationshipEdge edge, String queryString, Classification classification, List<Language> languages,
1714
            boolean highlightFragments, SortField[] sortFields) throws IOException {
1715

    
1716
        String fromField;
1717
        String queryTermField;
1718
        String toField = "id"; // TaxonBase.uuid
1719

    
1720
        if(edge.isBidirectional()){
1721
            throw new RuntimeException("Bidirectional joining not supported!");
1722
        }
1723
        if(edge.isEvers()){
1724
            fromField = "relatedFrom.id";
1725
            queryTermField = "relatedFrom.titleCache";
1726
        } else if(edge.isInvers()) {
1727
            fromField = "relatedTo.id";
1728
            queryTermField = "relatedTo.titleCache";
1729
        } else {
1730
            throw new RuntimeException("Invalid direction: " + edge.getDirections());
1731
        }
1732

    
1733
        Builder finalQueryBuilder = new Builder();
1734

    
1735
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1736
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1737

    
1738
        Builder joinFromQueryBuilder = new Builder();
1739
        joinFromQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(queryTermField, queryString), Occur.MUST);
1740
        joinFromQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery("type.id", edge.getTaxonRelationshipType()), Occur.MUST);
1741
        Query joinQuery = taxonBaseQueryFactory.newJoinQuery(TaxonRelationship.class, fromField, false, joinFromQueryBuilder.build(), toField, null, ScoreMode.Max);
1742

    
1743
        if(sortFields == null){
1744
            sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING,  false)};
1745
        }
1746
        luceneSearch.setSortFields(sortFields);
1747

    
1748
        finalQueryBuilder.add(joinQuery, Occur.MUST);
1749

    
1750
        if(classification != null){
1751
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery("taxonNodes.classification.id", classification), Occur.MUST);
1752
        }
1753
        luceneSearch.setQuery(finalQueryBuilder.build());
1754

    
1755
        if(highlightFragments){
1756
            luceneSearch.setHighlightFields(taxonBaseQueryFactory.getTextFieldNamesAsArray());
1757
        }
1758
        return luceneSearch;
1759
    }
1760

    
1761
    @Override
1762
    public Pager<SearchResult<TaxonBase>> findTaxaAndNamesByFullText(
1763
            EnumSet<TaxaAndNamesSearchMode> searchModes, String queryString, Classification classification,
1764
            Set<NamedArea> namedAreas, Set<PresenceAbsenceTerm> distributionStatus, List<Language> languages,
1765
            boolean highlightFragments, Integer pageSize,
1766
            Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths)
1767
            throws CorruptIndexException, IOException, ParseException, LuceneMultiSearchException {
1768

    
1769
        // FIXME: allow taxonomic ordering
1770
        //  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";
1771
        // this require building a special sort column by a special classBridge
1772
        if(highlightFragments){
1773
            logger.warn("findTaxaAndNamesByFullText() : fragment highlighting is " +
1774
                    "currently not fully supported by this method and thus " +
1775
                    "may not work with common names and misapplied names.");
1776
        }
1777

    
1778
        // convert sets to lists
1779
        List<NamedArea> namedAreaList = null;
1780
        List<PresenceAbsenceTerm>distributionStatusList = null;
1781
        if(namedAreas != null){
1782
            namedAreaList = new ArrayList<NamedArea>(namedAreas.size());
1783
            namedAreaList.addAll(namedAreas);
1784
        }
1785
        if(distributionStatus != null){
1786
            distributionStatusList = new ArrayList<PresenceAbsenceTerm>(distributionStatus.size());
1787
            distributionStatusList.addAll(distributionStatus);
1788
        }
1789

    
1790
        // set default if parameter is null
1791
        if(searchModes == null){
1792
            searchModes = EnumSet.of(TaxaAndNamesSearchMode.doTaxa);
1793
        }
1794

    
1795
        // set sort order and thus override any sort orders which may have been
1796
        // defined by prepare*Search methods
1797
        if(orderHints == null){
1798
            orderHints = OrderHint.NOMENCLATURAL_SORT_ORDER;
1799
        }
1800
        SortField[] sortFields = new SortField[orderHints.size()];
1801
        int i = 0;
1802
        for(OrderHint oh : orderHints){
1803
            sortFields[i++] = oh.toSortField();
1804
        }
1805
//        SortField[] sortFields = new SortField[]{SortField.FIELD_SCORE, new SortField("id", SortField.STRING, false)};
1806
//        SortField[] sortFields = new SortField[]{new SortField(NomenclaturalSortOrderBrigde.NAME_SORT_FIELD_NAME, SortField.STRING, false)};
1807

    
1808

    
1809
        boolean addDistributionFilter = namedAreas != null && namedAreas.size() > 0;
1810

    
1811
        List<LuceneSearch> luceneSearches = new ArrayList<LuceneSearch>();
1812
        Map<CdmBaseType, String> idFieldMap = new HashMap<CdmBaseType, String>();
1813

    
1814
        /*
1815
          ======== filtering by distribution , HOWTO ========
1816

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

    
1822

    
1823
          3. how does it work in spatial?
1824
          see
1825
           - http://www.nsshutdown.com/projects/lucene/whitepaper/locallucene_v2.html
1826
           - http://www.infoq.com/articles/LuceneSpatialSupport
1827
           - http://www.mhaller.de/archives/156-Spatial-search-with-Lucene.html
1828
          ------------------------------------------------------------------------
1829

    
1830
          filter strategies:
1831
          A) use a separate distribution filter per index sub-query/search:
1832
           - byTaxonSyonym (query TaxaonBase):
1833
               use a join area filter (Distribution -> TaxonBase)
1834
           - byCommonName (query DescriptionElementBase): use an area filter on
1835
               DescriptionElementBase !!! PROBLEM !!!
1836
               This cannot work since the distributions are different entities than the
1837
               common names and thus these are different lucene documents.
1838
           - byMisaplliedNames (join query TaxonRelationship -> TaxaonBase):
1839
               use a join area filter (Distribution -> TaxonBase)
1840

    
1841
          B) use a common distribution filter for all index sub-query/searches:
1842
           - use a common join area filter (Distribution -> TaxonBase)
1843
           - also implement the byCommonName as join query (CommonName -> TaxonBase)
1844
           PROBLEM in this case: we are losing the fragment highlighting for the
1845
           common names, since the returned documents are always TaxonBases
1846
        */
1847

    
1848
        /* The QueryFactory for creating filter queries on Distributions should
1849
         * The query factory used for the common names query cannot be reused
1850
         * for this case, since we want to only record the text fields which are
1851
         * actually used in the primary query
1852
         */
1853
        QueryFactory distributionFilterQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Distribution.class);
1854

    
1855
        Builder multiIndexByAreaFilterBuilder = new Builder();
1856

    
1857
        // search for taxa or synonyms
1858
        if(searchModes.contains(TaxaAndNamesSearchMode.doTaxa) || searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)) {
1859
            Class taxonBaseSubclass = TaxonBase.class;
1860
            if(searchModes.contains(TaxaAndNamesSearchMode.doTaxa) && !searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
1861
                taxonBaseSubclass = Taxon.class;
1862
            } else if (!searchModes.contains(TaxaAndNamesSearchMode.doTaxa) && searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)) {
1863
                taxonBaseSubclass = Synonym.class;
1864
            }
1865
            luceneSearches.add(prepareFindByFullTextSearch(taxonBaseSubclass, queryString, classification, languages, highlightFragments, sortFields));
1866
            idFieldMap.put(CdmBaseType.TAXON, "id");
1867
            /* A) does not work!!!!
1868
            if(addDistributionFilter){
1869
                // in this case we need a filter which uses a join query
1870
                // to get the TaxonBase documents for the DescriptionElementBase documents
1871
                // which are matching the areas in question
1872
                Query taxonAreaJoinQuery = createByDistributionJoinQuery(
1873
                        namedAreaList,
1874
                        distributionStatusList,
1875
                        distributionFilterQueryFactory
1876
                        );
1877
                multiIndexByAreaFilter.add(new QueryWrapperFilter(taxonAreaJoinQuery), Occur.SHOULD);
1878
            }
1879
            */
1880
            if(addDistributionFilter && searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
1881
                // add additional area filter for synonyms
1882
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1883
                String toField = "accTaxon.id"; // id in TaxonBase index (is multivalued)
1884

    
1885
                //TODO replace by createByDistributionJoinQuery
1886
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
1887
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class, fromField, true, byDistributionQuery, toField, Taxon.class, ScoreMode.None);
1888
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1889

    
1890
            }
1891
        }
1892

    
1893
        // search by CommonTaxonName
1894
        if(searchModes.contains(TaxaAndNamesSearchMode.doTaxaByCommonNames)) {
1895
            // B)
1896
            QueryFactory descriptionElementQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(DescriptionElementBase.class);
1897
            Query byCommonNameJoinQuery = descriptionElementQueryFactory.newJoinQuery(
1898
                    CommonTaxonName.class,
1899
                    "inDescription.taxon.id",
1900
                    true,
1901
                    QueryFactory.addTypeRestriction(
1902
                                createByDescriptionElementFullTextQuery(queryString, classification, null, languages, descriptionElementQueryFactory)
1903
                                , CommonTaxonName.class
1904
                                ).build(), "id", null, ScoreMode.Max);
1905
            logger.debug("byCommonNameJoinQuery: " + byCommonNameJoinQuery.toString());
1906
            LuceneSearch byCommonNameSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
1907
            byCommonNameSearch.setCdmTypRestriction(Taxon.class);
1908
            byCommonNameSearch.setQuery(byCommonNameJoinQuery);
1909
            byCommonNameSearch.setSortFields(sortFields);
1910
            idFieldMap.put(CdmBaseType.TAXON, "id");
1911

    
1912
            luceneSearches.add(byCommonNameSearch);
1913

    
1914
            /* A) does not work!!!!
1915
            luceneSearches.add(
1916
                    prepareByDescriptionElementFullTextSearch(CommonTaxonName.class,
1917
                            queryString, classification, null, languages, highlightFragments)
1918
                        );
1919
            idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
1920
            if(addDistributionFilter){
1921
                // in this case we are able to use DescriptionElementBase documents
1922
                // which are matching the areas in question directly
1923
                BooleanQuery byDistributionQuery = createByDistributionQuery(
1924
                        namedAreaList,
1925
                        distributionStatusList,
1926
                        distributionFilterQueryFactory
1927
                        );
1928
                multiIndexByAreaFilter.add(new QueryWrapperFilter(byDistributionQuery), Occur.SHOULD);
1929
            } */
1930
        }
1931

    
1932
        // search by misapplied names
1933
        if(searchModes.contains(TaxaAndNamesSearchMode.doMisappliedNames)) {
1934
            // NOTE:
1935
            // prepareFindByTaxonRelationFullTextSearch() is making use of JoinUtil.createJoinQuery()
1936
            // which allows doing query time joins
1937
            // finds the misapplied name (Taxon B) which is an misapplication for
1938
            // a related Taxon A.
1939
            //
1940
            luceneSearches.add(prepareFindByTaxonRelationFullTextSearch(
1941
                    new TaxonRelationshipEdge(TaxonRelationshipType.MISAPPLIED_NAME_FOR(), Direction.relatedTo),
1942
                    queryString, classification, languages, highlightFragments, sortFields));
1943
            idFieldMap.put(CdmBaseType.TAXON, "id");
1944

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

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

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

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

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

    
1988
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider,
1989
                luceneSearches.toArray(new LuceneSearch[luceneSearches.size()]));
1990

    
1991

    
1992
        if(addDistributionFilter){
1993

    
1994
            // B)
1995
            // in this case we need a filter which uses a join query
1996
            // to get the TaxonBase documents for the DescriptionElementBase documents
1997
            // which are matching the areas in question
1998
            //
1999
            // for toTaxa, doByCommonName
2000
            Query taxonAreaJoinQuery = createByDistributionJoinQuery(
2001
                    namedAreaList,
2002
                    distributionStatusList,
2003
                    distributionFilterQueryFactory,
2004
                    Taxon.class, true
2005
                    );
2006
            multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
2007
        }
2008

    
2009
        if (addDistributionFilter){
2010
            multiSearch.setFilter(multiIndexByAreaFilterBuilder.build());
2011
        }
2012

    
2013

    
2014
        // --- execute search
2015
        TopGroups<BytesRef> topDocsResultSet = multiSearch.executeSearch(pageSize, pageNumber);
2016

    
2017
        // --- initialize taxa, highlight matches ....
2018
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
2019

    
2020

    
2021
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2022
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2023

    
2024
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
2025
        return new DefaultPagerImpl<SearchResult<TaxonBase>>(pageNumber, totalHits, pageSize, searchResults);
2026
    }
2027

    
2028
    /**
2029
     * @param namedAreaList at least one area must be in the list
2030
     * @param distributionStatusList optional
2031
     * @param toType toType
2032
     *      Optional parameter. Only used for debugging to print the toType documents
2033
     * @param asFilter TODO
2034
     * @return
2035
     * @throws IOException
2036
     */
2037
    protected Query createByDistributionJoinQuery(
2038
            List<NamedArea> namedAreaList,
2039
            List<PresenceAbsenceTerm> distributionStatusList,
2040
            QueryFactory queryFactory, Class<? extends CdmBase> toType, boolean asFilter
2041
            ) throws IOException {
2042

    
2043
        String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
2044
        String toField = "id"; // id in toType usually this is the TaxonBase index
2045

    
2046
        BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, queryFactory);
2047

    
2048
        ScoreMode scoreMode = asFilter ?  ScoreMode.None : ScoreMode.Max;
2049

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

    
2052
        return taxonAreaJoinQuery;
2053
    }
2054

    
2055
    /**
2056
     * @param namedAreaList
2057
     * @param distributionStatusList
2058
     * @param queryFactory
2059
     * @return
2060
     */
2061
    private BooleanQuery createByDistributionQuery(List<NamedArea> namedAreaList,
2062
            List<PresenceAbsenceTerm> distributionStatusList, QueryFactory queryFactory) {
2063
        Builder areaQueryBuilder = new Builder();
2064
        // area field from Distribution
2065
        areaQueryBuilder.add(queryFactory.newEntityIdsQuery("area.id", namedAreaList), Occur.MUST);
2066

    
2067
        // status field from Distribution
2068
        if(distributionStatusList != null && distributionStatusList.size() > 0){
2069
            areaQueryBuilder.add(queryFactory.newEntityIdsQuery("status.id", distributionStatusList), Occur.MUST);
2070
        }
2071

    
2072
        BooleanQuery areaQuery = areaQueryBuilder.build();
2073
        logger.debug("createByDistributionQuery() query: " + areaQuery.toString());
2074
        return areaQuery;
2075
    }
2076

    
2077
    /**
2078
     * This method has been primarily created for testing the area join query but might
2079
     * also be useful in other situations
2080
     *
2081
     * @param namedAreaList
2082
     * @param distributionStatusList
2083
     * @param classification
2084
     * @param highlightFragments
2085
     * @return
2086
     * @throws IOException
2087
     */
2088
    protected LuceneSearch prepareByDistributionSearch(
2089
            List<NamedArea> namedAreaList, List<PresenceAbsenceTerm> distributionStatusList,
2090
            Classification classification) throws IOException {
2091

    
2092
        Builder finalQueryBuilder = new Builder();
2093

    
2094
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
2095

    
2096
        // FIXME is this query factory using the wrong type?
2097
        QueryFactory taxonQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Taxon.class);
2098

    
2099
        SortField[] sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING, false)};
2100
        luceneSearch.setSortFields(sortFields);
2101

    
2102

    
2103
        Query byAreaQuery = createByDistributionJoinQuery(namedAreaList, distributionStatusList, taxonQueryFactory, null, false);
2104

    
2105
        finalQueryBuilder.add(byAreaQuery, Occur.MUST);
2106

    
2107
        if(classification != null){
2108
            finalQueryBuilder.add(taxonQueryFactory.newEntityIdQuery("taxonNodes.classification.id", classification), Occur.MUST);
2109
        }
2110
        BooleanQuery finalQuery = finalQueryBuilder.build();
2111
        logger.info("prepareByAreaSearch() query: " + finalQuery.toString());
2112
        luceneSearch.setQuery(finalQuery);
2113

    
2114
        return luceneSearch;
2115
    }
2116

    
2117
    @Override
2118
    public Pager<SearchResult<TaxonBase>> findByDescriptionElementFullText(
2119
            Class<? extends DescriptionElementBase> clazz, String queryString,
2120
            Classification classification, List<Feature> features, List<Language> languages,
2121
            boolean highlightFragments, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws CorruptIndexException, IOException, ParseException {
2122

    
2123

    
2124
        LuceneSearch luceneSearch = prepareByDescriptionElementFullTextSearch(clazz, queryString, classification, features, languages, highlightFragments);
2125

    
2126
        // --- execute search
2127
        TopGroups<BytesRef> topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
2128

    
2129
        Map<CdmBaseType, String> idFieldMap = new HashMap<CdmBaseType, String>();
2130
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2131

    
2132
        // --- initialize taxa, highlight matches ....
2133
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
2134
        @SuppressWarnings("rawtypes")
2135
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2136
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2137

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

    
2141
    }
2142

    
2143

    
2144
    @Override
2145
    public Pager<SearchResult<TaxonBase>> findByEverythingFullText(String queryString,
2146
            Classification classification, List<Language> languages, boolean highlightFragments,
2147
            Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws CorruptIndexException, IOException, ParseException, LuceneMultiSearchException {
2148

    
2149
        LuceneSearch luceneSearchByDescriptionElement = prepareByDescriptionElementFullTextSearch(null, queryString, classification, null, languages, highlightFragments);
2150
        LuceneSearch luceneSearchByTaxonBase = prepareFindByFullTextSearch(null, queryString, classification, languages, highlightFragments, null);
2151

    
2152
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider, luceneSearchByDescriptionElement, luceneSearchByTaxonBase);
2153

    
2154
        // --- execute search
2155
        TopGroups<BytesRef> topDocsResultSet = multiSearch.executeSearch(pageSize, pageNumber);
2156

    
2157
        // --- initialize taxa, highlight matches ....
2158
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
2159

    
2160
        Map<CdmBaseType, String> idFieldMap = new HashMap<CdmBaseType, String>();
2161
        idFieldMap.put(CdmBaseType.TAXON, "id");
2162
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2163

    
2164
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2165
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2166

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

    
2170
    }
2171

    
2172

    
2173
    /**
2174
     * @param clazz
2175
     * @param queryString
2176
     * @param classification
2177
     * @param features
2178
     * @param languages
2179
     * @param highlightFragments
2180
     * @param directorySelectClass
2181
     * @return
2182
     */
2183
    protected LuceneSearch prepareByDescriptionElementFullTextSearch(Class<? extends CdmBase> clazz,
2184
            String queryString, Classification classification, List<Feature> features,
2185
            List<Language> languages, boolean highlightFragments) {
2186

    
2187
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, DescriptionElementBase.class);
2188
        QueryFactory descriptionElementQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(DescriptionElementBase.class);
2189

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

    
2192
        BooleanQuery finalQuery = createByDescriptionElementFullTextQuery(queryString, classification, features,
2193
                languages, descriptionElementQueryFactory);
2194

    
2195
        luceneSearch.setSortFields(sortFields);
2196
        luceneSearch.setCdmTypRestriction(clazz);
2197
        luceneSearch.setQuery(finalQuery);
2198
        if(highlightFragments){
2199
            luceneSearch.setHighlightFields(descriptionElementQueryFactory.getTextFieldNamesAsArray());
2200
        }
2201

    
2202
        return luceneSearch;
2203
    }
2204

    
2205
    /**
2206
     * @param queryString
2207
     * @param classification
2208
     * @param features
2209
     * @param languages
2210
     * @param descriptionElementQueryFactory
2211
     * @return
2212
     */
2213
    private BooleanQuery createByDescriptionElementFullTextQuery(String queryString, Classification classification,
2214
            List<Feature> features, List<Language> languages, QueryFactory descriptionElementQueryFactory) {
2215
        Builder finalQueryBuilder = new Builder();
2216
        Builder textQueryBuilder = new Builder();
2217
        textQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
2218

    
2219
        // common name
2220
        Builder nameQueryBuilder = new Builder();
2221
        if(languages == null || languages.size() == 0){
2222
            nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2223
        } else {
2224
            Builder languageSubQueryBuilder = new Builder();
2225
            for(Language lang : languages){
2226
                languageSubQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("language.uuid",  lang.getUuid().toString(), false), Occur.SHOULD);
2227
            }
2228
            nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2229
            nameQueryBuilder.add(languageSubQueryBuilder.build(), Occur.MUST);
2230
        }
2231
        textQueryBuilder.add(nameQueryBuilder.build(), Occur.SHOULD);
2232

    
2233

    
2234
        // text field from TextData
2235
        textQueryBuilder.add(descriptionElementQueryFactory.newMultilanguageTextQuery("text", queryString, languages), Occur.SHOULD);
2236

    
2237
        // --- TermBase fields - by representation ----
2238
        // state field from CategoricalData
2239
        textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.state", queryString, languages), Occur.SHOULD);
2240

    
2241
        // state field from CategoricalData
2242
        textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.modifyingText", queryString, languages), Occur.SHOULD);
2243

    
2244
        // area field from Distribution
2245
        textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("area", queryString, languages), Occur.SHOULD);
2246

    
2247
        // status field from Distribution
2248
        textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("status", queryString, languages), Occur.SHOULD);
2249

    
2250
        finalQueryBuilder.add(textQueryBuilder.build(), Occur.MUST);
2251
        // --- classification ----
2252

    
2253
        if(classification != null){
2254
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityIdQuery("inDescription.taxon.taxonNodes.classification.id", classification), Occur.MUST);
2255
        }
2256

    
2257
        // --- IdentifieableEntity fields - by uuid
2258
        if(features != null && features.size() > 0 ){
2259
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityUuidsQuery("feature.uuid", features), Occur.MUST);
2260
        }
2261

    
2262
        // the description must be associated with a taxon
2263
        finalQueryBuilder.add(descriptionElementQueryFactory.newIsNotNullQuery("inDescription.taxon.id"), Occur.MUST);
2264

    
2265
        BooleanQuery finalQuery = finalQueryBuilder.build();
2266
        logger.info("prepareByDescriptionElementFullTextSearch() query: " + finalQuery.toString());
2267
        return finalQuery;
2268
    }
2269

    
2270
    /**
2271
     * DefinedTerm representations and MultilanguageString maps are stored in the Lucene index by the {@link DefinedTermBaseClassBridge}
2272
     * and {@link MultilanguageTextFieldBridge } in a consistent way. One field per language and also in one additional field for all languages.
2273
     * This method is a convenient means to retrieve a Lucene query string for such the fields.
2274
     *
2275
     * @param name name of the term field as in the Lucene index. Must be field created by {@link DefinedTermBaseClassBridge}
2276
     * or {@link MultilanguageTextFieldBridge }
2277
     * @param languages the languages to search for exclusively. Can be <code>null</code> to search in all languages
2278
     * @param stringBuilder a StringBuilder to be reused, if <code>null</code> a new StringBuilder will be instantiated and is returned
2279
     * @return the StringBuilder given a parameter or a new one if the stringBuilder parameter was null.
2280
     *
2281
     * TODO move to utiliy class !!!!!!!!
2282
     */
2283
    private StringBuilder appendLocalizedFieldQuery(String name, List<Language> languages, StringBuilder stringBuilder) {
2284

    
2285
        if(stringBuilder == null){
2286
            stringBuilder = new StringBuilder();
2287
        }
2288
        if(languages == null || languages.size() == 0){
2289
            stringBuilder.append(name + ".ALL:(%1$s) ");
2290
        } else {
2291
            for(Language lang : languages){
2292
                stringBuilder.append(name + "." + lang.getUuid().toString() + ":(%1$s) ");
2293
            }
2294
        }
2295
        return stringBuilder;
2296
    }
2297

    
2298
    @Override
2299
    public List<Synonym> createInferredSynonyms(Taxon taxon, Classification classification, SynonymRelationshipType type, boolean doWithMisappliedNames){
2300
        List <Synonym> inferredSynonyms = new ArrayList<Synonym>();
2301
        List<Synonym> inferredSynonymsToBeRemoved = new ArrayList<Synonym>();
2302

    
2303
        HashMap <UUID, ZoologicalName> zooHashMap = new HashMap<UUID, ZoologicalName>();
2304

    
2305

    
2306
        UUID nameUuid= taxon.getName().getUuid();
2307
        ZoologicalName taxonName = getZoologicalName(nameUuid, zooHashMap);
2308
        String epithetOfTaxon = null;
2309
        String infragenericEpithetOfTaxon = null;
2310
        String infraspecificEpithetOfTaxon = null;
2311
        if (taxonName.isSpecies()){
2312
             epithetOfTaxon= taxonName.getSpecificEpithet();
2313
        } else if (taxonName.isInfraGeneric()){
2314
            infragenericEpithetOfTaxon = taxonName.getInfraGenericEpithet();
2315
        } else if (taxonName.isInfraSpecific()){
2316
            infraspecificEpithetOfTaxon = taxonName.getInfraSpecificEpithet();
2317
        }
2318
        String genusOfTaxon = taxonName.getGenusOrUninomial();
2319
        Set<TaxonNode> nodes = taxon.getTaxonNodes();
2320
        List<String> taxonNames = new ArrayList<String>();
2321

    
2322
        for (TaxonNode node: nodes){
2323
           // HashMap<String, String> synonymsGenus = new HashMap<String, String>(); // Changed this to be able to store the idInSource to a genusName
2324
           // List<String> synonymsEpithet = new ArrayList<String>();
2325

    
2326
            if (node.getClassification().equals(classification)){
2327
                if (!node.isTopmostNode()){
2328
                    TaxonNode parent = node.getParent();
2329
                    parent = HibernateProxyHelper.deproxy(parent);
2330
                    TaxonNameBase<?,?> parentName =  parent.getTaxon().getName();
2331
                    ZoologicalName zooParentName = HibernateProxyHelper.deproxy(parentName, ZoologicalName.class);
2332
                    Taxon parentTaxon = HibernateProxyHelper.deproxy(parent.getTaxon());
2333
                    Rank rankOfTaxon = taxonName.getRank();
2334

    
2335

    
2336
                    //create inferred synonyms for species, subspecies
2337
                    if ((parentName.isGenus() || parentName.isSpecies() || parentName.getRank().equals(Rank.SUBGENUS())) ){
2338

    
2339
                        Synonym inferredEpithet = null;
2340
                        Synonym inferredGenus = null;
2341
                        Synonym potentialCombination = null;
2342

    
2343
                        List<String> propertyPaths = new ArrayList<String>();
2344
                        propertyPaths.add("synonym");
2345
                        propertyPaths.add("synonym.name");
2346
                        List<OrderHint> orderHints = new ArrayList<OrderHint>();
2347
                        orderHints.add(new OrderHint("relatedFrom.titleCache", SortOrder.ASCENDING));
2348

    
2349
                        List<SynonymRelationship> synonymRelationshipsOfParent = dao.getSynonyms(parentTaxon, SynonymRelationshipType.HETEROTYPIC_SYNONYM_OF(), null, null,orderHints,propertyPaths);
2350
                        List<SynonymRelationship> synonymRelationshipsOfTaxon= dao.getSynonyms(taxon, SynonymRelationshipType.HETEROTYPIC_SYNONYM_OF(), null, null,orderHints,propertyPaths);
2351

    
2352
                        List<TaxonRelationship> taxonRelListParent = null;
2353
                        List<TaxonRelationship> taxonRelListTaxon = null;
2354
                        if (doWithMisappliedNames){
2355
                            taxonRelListParent = dao.getTaxonRelationships(parentTaxon, TaxonRelationshipType.MISAPPLIED_NAME_FOR(), null, null, orderHints, propertyPaths, Direction.relatedTo);
2356
                            taxonRelListTaxon = dao.getTaxonRelationships(taxon, TaxonRelationshipType.MISAPPLIED_NAME_FOR(), null, null, orderHints, propertyPaths, Direction.relatedTo);
2357
                        }
2358

    
2359

    
2360
                        if (type.equals(SynonymRelationshipType.INFERRED_EPITHET_OF())){
2361

    
2362

    
2363
                            for (SynonymRelationship synonymRelationOfParent:synonymRelationshipsOfParent){
2364
                                Synonym syn = synonymRelationOfParent.getSynonym();
2365

    
2366
                                inferredEpithet = createInferredEpithets(taxon,
2367
                                        zooHashMap, taxonName, epithetOfTaxon,
2368
                                        infragenericEpithetOfTaxon,
2369
                                        infraspecificEpithetOfTaxon,
2370
                                        taxonNames, parentName,
2371
                                        syn);
2372

    
2373

    
2374
                                inferredSynonyms.add(inferredEpithet);
2375
                                zooHashMap.put(inferredEpithet.getName().getUuid(), (ZoologicalName)inferredEpithet.getName());
2376
                                taxonNames.add(((ZoologicalName)inferredEpithet.getName()).getNameCache());
2377
                            }
2378

    
2379
                            if (doWithMisappliedNames){
2380

    
2381
                                for (TaxonRelationship taxonRelationship: taxonRelListParent){
2382
                                     Taxon misappliedName = taxonRelationship.getFromTaxon();
2383

    
2384
                                     inferredEpithet = createInferredEpithets(taxon,
2385
                                             zooHashMap, taxonName, epithetOfTaxon,
2386
                                             infragenericEpithetOfTaxon,
2387
                                             infraspecificEpithetOfTaxon,
2388
                                             taxonNames, parentName,
2389
                                             misappliedName);
2390

    
2391
                                    inferredSynonyms.add(inferredEpithet);
2392
                                    zooHashMap.put(inferredEpithet.getName().getUuid(), (ZoologicalName)inferredEpithet.getName());
2393
                                     taxonNames.add(((ZoologicalName)inferredEpithet.getName()).getNameCache());
2394
                                }
2395
                            }
2396

    
2397
                            if (!taxonNames.isEmpty()){
2398
                            List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2399
                            ZoologicalName name;
2400
                            if (!synNotInCDM.isEmpty()){
2401
                                inferredSynonymsToBeRemoved.clear();
2402

    
2403
                                for (Synonym syn :inferredSynonyms){
2404
                                    name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2405
                                    if (!synNotInCDM.contains(name.getNameCache())){
2406
                                        inferredSynonymsToBeRemoved.add(syn);
2407
                                    }
2408
                                }
2409

    
2410
                                // Remove identified Synonyms from inferredSynonyms
2411
                                for (Synonym synonym : inferredSynonymsToBeRemoved) {
2412
                                    inferredSynonyms.remove(synonym);
2413
                                }
2414
                            }
2415
                        }
2416

    
2417
                    }else if (type.equals(SynonymRelationshipType.INFERRED_GENUS_OF())){
2418

    
2419

    
2420
                        for (SynonymRelationship synonymRelationOfTaxon:synonymRelationshipsOfTaxon){
2421
                            TaxonNameBase synName;
2422
                            ZoologicalName inferredSynName;
2423

    
2424
                            Synonym syn = synonymRelationOfTaxon.getSynonym();
2425
                            inferredGenus = createInferredGenus(taxon,
2426
                                    zooHashMap, taxonName, epithetOfTaxon,
2427
                                    genusOfTaxon, taxonNames, zooParentName, syn);
2428

    
2429
                            inferredSynonyms.add(inferredGenus);
2430
                            zooHashMap.put(inferredGenus.getName().getUuid(), (ZoologicalName)inferredGenus.getName());
2431
                            taxonNames.add(( (ZoologicalName)inferredGenus.getName()).getNameCache());
2432

    
2433

    
2434
                        }
2435

    
2436
                        if (doWithMisappliedNames){
2437

    
2438
                            for (TaxonRelationship taxonRelationship: taxonRelListTaxon){
2439
                                Taxon misappliedName = taxonRelationship.getFromTaxon();
2440
                                inferredGenus = createInferredGenus(taxon, zooHashMap, taxonName, infraspecificEpithetOfTaxon, genusOfTaxon, taxonNames, zooParentName,  misappliedName);
2441

    
2442
                                inferredSynonyms.add(inferredGenus);
2443
                                zooHashMap.put(inferredGenus.getName().getUuid(), (ZoologicalName)inferredGenus.getName());
2444
                                 taxonNames.add(( (ZoologicalName)inferredGenus.getName()).getNameCache());
2445
                            }
2446
                        }
2447

    
2448

    
2449
                        if (!taxonNames.isEmpty()){
2450
                            List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2451
                            ZoologicalName name;
2452
                            if (!synNotInCDM.isEmpty()){
2453
                                inferredSynonymsToBeRemoved.clear();
2454

    
2455
                                for (Synonym syn :inferredSynonyms){
2456
                                    name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2457
                                    if (!synNotInCDM.contains(name.getNameCache())){
2458
                                        inferredSynonymsToBeRemoved.add(syn);
2459
                                    }
2460
                                }
2461

    
2462
                                // Remove identified Synonyms from inferredSynonyms
2463
                                for (Synonym synonym : inferredSynonymsToBeRemoved) {
2464
                                    inferredSynonyms.remove(synonym);
2465
                                }
2466
                            }
2467
                        }
2468

    
2469
                    }else if (type.equals(SynonymRelationshipType.POTENTIAL_COMBINATION_OF())){
2470

    
2471
                        Reference sourceReference = null; // TODO: Determination of sourceReference is redundant
2472
                        ZoologicalName inferredSynName;
2473
                        //for all synonyms of the parent...
2474
                        for (SynonymRelationship synonymRelationOfParent:synonymRelationshipsOfParent){
2475
                            TaxonNameBase synName;
2476
                            Synonym synParent = synonymRelationOfParent.getSynonym();
2477
                            synName = synParent.getName();
2478

    
2479
                            HibernateProxyHelper.deproxy(synParent);
2480

    
2481
                            // Set the sourceReference
2482
                            sourceReference = synParent.getSec();
2483

    
2484
                            // Determine the idInSource
2485
                            String idInSourceParent = getIdInSource(synParent);
2486

    
2487
                            ZoologicalName parentSynZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2488
                            String synParentGenus = parentSynZooName.getGenusOrUninomial();
2489
                            String synParentInfragenericName = null;
2490
                            String synParentSpecificEpithet = null;
2491

    
2492
                            if (parentSynZooName.isInfraGeneric()){
2493
                                synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2494
                            }
2495
                            if (parentSynZooName.isSpecies()){
2496
                                synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2497
                            }
2498

    
2499
                           /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2500
                                synonymsGenus.put(synGenusName, idInSource);
2501
                            }*/
2502

    
2503
                            //for all synonyms of the taxon
2504

    
2505
                            for (SynonymRelationship synonymRelationOfTaxon:synonymRelationshipsOfTaxon){
2506

    
2507
                                Synonym syn = synonymRelationOfTaxon.getSynonym();
2508
                                ZoologicalName zooSynName = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2509
                                potentialCombination = createPotentialCombination(idInSourceParent, parentSynZooName, zooSynName,
2510
                                        synParentGenus,
2511
                                        synParentInfragenericName,
2512
                                        synParentSpecificEpithet, syn, zooHashMap);
2513

    
2514
                                taxon.addSynonym(potentialCombination, SynonymRelationshipType.POTENTIAL_COMBINATION_OF());
2515
                                inferredSynonyms.add(potentialCombination);
2516
                                zooHashMap.put(potentialCombination.getName().getUuid(), (ZoologicalName)potentialCombination.getName());
2517
                                 taxonNames.add(( (ZoologicalName)potentialCombination.getName()).getNameCache());
2518

    
2519
                            }
2520

    
2521

    
2522
                        }
2523

    
2524
                        if (doWithMisappliedNames){
2525

    
2526
                            for (TaxonRelationship parentRelationship: taxonRelListParent){
2527

    
2528
                                TaxonNameBase misappliedParentName;
2529

    
2530
                                Taxon misappliedParent = parentRelationship.getFromTaxon();
2531
                                misappliedParentName = misappliedParent.getName();
2532

    
2533
                                HibernateProxyHelper.deproxy(misappliedParent);
2534

    
2535
                                // Set the sourceReference
2536
                                sourceReference = misappliedParent.getSec();
2537

    
2538
                                // Determine the idInSource
2539
                                String idInSourceParent = getIdInSource(misappliedParent);
2540

    
2541
                                ZoologicalName parentSynZooName = getZoologicalName(misappliedParentName.getUuid(), zooHashMap);
2542
                                String synParentGenus = parentSynZooName.getGenusOrUninomial();
2543
                                String synParentInfragenericName = null;
2544
                                String synParentSpecificEpithet = null;
2545

    
2546
                                if (parentSynZooName.isInfraGeneric()){
2547
                                    synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2548
                                }
2549
                                if (parentSynZooName.isSpecies()){
2550
                                    synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2551
                                }
2552

    
2553

    
2554
                                for (TaxonRelationship taxonRelationship: taxonRelListTaxon){
2555
                                    Taxon misappliedName = taxonRelationship.getFromTaxon();
2556
                                    ZoologicalName zooMisappliedName = getZoologicalName(misappliedName.getName().getUuid(), zooHashMap);
2557
                                    potentialCombination = createPotentialCombination(
2558
                                            idInSourceParent, parentSynZooName, zooMisappliedName,
2559
                                            synParentGenus,
2560
                                            synParentInfragenericName,
2561
                                            synParentSpecificEpithet, misappliedName, zooHashMap);
2562

    
2563

    
2564
                                    taxon.addSynonym(potentialCombination, SynonymRelationshipType.POTENTIAL_COMBINATION_OF());
2565
                                    inferredSynonyms.add(potentialCombination);
2566
                                    zooHashMap.put(potentialCombination.getName().getUuid(), (ZoologicalName)potentialCombination.getName());
2567
                                     taxonNames.add(( (ZoologicalName)potentialCombination.getName()).getNameCache());
2568
                                }
2569
                            }
2570
                        }
2571

    
2572
                        if (!taxonNames.isEmpty()){
2573
                            List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2574
                            ZoologicalName name;
2575
                            if (!synNotInCDM.isEmpty()){
2576
                                inferredSynonymsToBeRemoved.clear();
2577
                                for (Synonym syn :inferredSynonyms){
2578
                                    try{
2579
                                        name = (ZoologicalName) syn.getName();
2580
                                    }catch (ClassCastException e){
2581
                                        name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2582
                                    }
2583
                                    if (!synNotInCDM.contains(name.getNameCache())){
2584
                                        inferredSynonymsToBeRemoved.add(syn);
2585
                                    }
2586
                                 }
2587
                                // Remove identified Synonyms from inferredSynonyms
2588
                                for (Synonym synonym : inferredSynonymsToBeRemoved) {
2589
                                    inferredSynonyms.remove(synonym);
2590
                                }
2591
                            }
2592
                         }
2593
                        }
2594
                    }else {
2595
                        logger.info("The synonymrelationship type is not defined.");
2596
                        return inferredSynonyms;
2597
                    }
2598
                }
2599
            }
2600

    
2601
        }
2602

    
2603
        return inferredSynonyms;
2604
    }
2605

    
2606
    private Synonym createPotentialCombination(String idInSourceParent,
2607
            ZoologicalName parentSynZooName, 	ZoologicalName zooSynName, String synParentGenus,
2608
            String synParentInfragenericName, String synParentSpecificEpithet,
2609
            TaxonBase syn, HashMap<UUID, ZoologicalName> zooHashMap) {
2610
        Synonym potentialCombination;
2611
        Reference sourceReference;
2612
        ZoologicalName inferredSynName;
2613
        HibernateProxyHelper.deproxy(syn);
2614

    
2615
        // Set sourceReference
2616
        sourceReference = syn.getSec();
2617
        if (sourceReference == null){
2618
            logger.warn("The synonym has no sec reference because it is a misapplied name! Take the sec reference of taxon");
2619
            //TODO:Remove
2620
            if (!parentSynZooName.getTaxa().isEmpty()){
2621
                TaxonBase taxon = parentSynZooName.getTaxa().iterator().next();
2622

    
2623
                sourceReference = taxon.getSec();
2624
            }
2625
        }
2626
        String synTaxonSpecificEpithet = zooSynName.getSpecificEpithet();
2627

    
2628
        String synTaxonInfraSpecificName= null;
2629

    
2630
        if (parentSynZooName.isSpecies()){
2631
            synTaxonInfraSpecificName = zooSynName.getInfraSpecificEpithet();
2632
        }
2633

    
2634
        /*if (epithetName != null && !synonymsEpithet.contains(epithetName)){
2635
            synonymsEpithet.add(epithetName);
2636
        }*/
2637

    
2638
        //create potential combinations...
2639
        inferredSynName = ZoologicalName.NewInstance(syn.getName().getRank());
2640

    
2641
        inferredSynName.setGenusOrUninomial(synParentGenus);
2642
        if (zooSynName.isSpecies()){
2643
              inferredSynName.setSpecificEpithet(synTaxonSpecificEpithet);
2644
              if (parentSynZooName.isInfraGeneric()){
2645
                  inferredSynName.setInfraGenericEpithet(synParentInfragenericName);
2646
              }
2647
        }
2648
        if (zooSynName.isInfraSpecific()){
2649
            inferredSynName.setSpecificEpithet(synParentSpecificEpithet);
2650
            inferredSynName.setInfraSpecificEpithet(synTaxonInfraSpecificName);
2651
        }
2652
        if (parentSynZooName.isInfraGeneric()){
2653
            inferredSynName.setInfraGenericEpithet(synParentInfragenericName);
2654
        }
2655

    
2656

    
2657
        potentialCombination = Synonym.NewInstance(inferredSynName, null);
2658

    
2659
        // Set the sourceReference
2660
        potentialCombination.setSec(sourceReference);
2661

    
2662

    
2663
        // Determine the idInSource
2664
        String idInSourceSyn= getIdInSource(syn);
2665

    
2666
        if (idInSourceParent != null && idInSourceSyn != null) {
2667
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2668
            inferredSynName.addSource(originalSource);
2669
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2670
            potentialCombination.addSource(originalSource);
2671
        }
2672

    
2673
        return potentialCombination;
2674
    }
2675

    
2676
    private Synonym createInferredGenus(Taxon taxon,
2677
            HashMap<UUID, ZoologicalName> zooHashMap, ZoologicalName taxonName,
2678
            String epithetOfTaxon, String genusOfTaxon,
2679
            List<String> taxonNames, ZoologicalName zooParentName,
2680
            TaxonBase syn) {
2681

    
2682
        Synonym inferredGenus;
2683
        TaxonNameBase synName;
2684
        ZoologicalName inferredSynName;
2685
        synName =syn.getName();
2686
        HibernateProxyHelper.deproxy(syn);
2687

    
2688
        // Determine the idInSource
2689
        String idInSourceSyn = getIdInSource(syn);
2690
        String idInSourceTaxon = getIdInSource(taxon);
2691
        // Determine the sourceReference
2692
        Reference sourceReference = syn.getSec();
2693

    
2694
        //logger.warn(sourceReference.getTitleCache());
2695

    
2696
        synName = syn.getName();
2697
        ZoologicalName synZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2698
        String synSpeciesEpithetName = synZooName.getSpecificEpithet();
2699
                     /* if (synonymsEpithet != null && !synonymsEpithet.contains(synSpeciesEpithetName)){
2700
            synonymsEpithet.add(synSpeciesEpithetName);
2701
        }*/
2702

    
2703
        inferredSynName = ZoologicalName.NewInstance(taxon.getName().getRank());
2704
        //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...
2705

    
2706

    
2707
        inferredSynName.setGenusOrUninomial(genusOfTaxon);
2708
        if (zooParentName.isInfraGeneric()){
2709
            inferredSynName.setInfraGenericEpithet(zooParentName.getInfraGenericEpithet());
2710
        }
2711

    
2712
        if (taxonName.isSpecies()){
2713
            inferredSynName.setSpecificEpithet(synSpeciesEpithetName);
2714
        }
2715
        if (taxonName.isInfraSpecific()){
2716
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2717
            inferredSynName.setInfraSpecificEpithet(synZooName.getInfraGenericEpithet());
2718
        }
2719

    
2720

    
2721
        inferredGenus = Synonym.NewInstance(inferredSynName, null);
2722

    
2723
        // Set the sourceReference
2724
        inferredGenus.setSec(sourceReference);
2725

    
2726
        // Add the original source
2727
        if (idInSourceSyn != null && idInSourceTaxon != null) {
2728
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2729
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2730
            inferredGenus.addSource(originalSource);
2731

    
2732
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2733
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2734
            inferredSynName.addSource(originalSource);
2735
            originalSource = null;
2736

    
2737
        }else{
2738
            logger.error("There is an idInSource missing: " + idInSourceSyn + " of Synonym or " + idInSourceTaxon + " of Taxon");
2739
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2740
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2741
            inferredGenus.addSource(originalSource);
2742

    
2743
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2744
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2745
            inferredSynName.addSource(originalSource);
2746
            originalSource = null;
2747
        }
2748

    
2749
        taxon.addSynonym(inferredGenus, SynonymRelationshipType.INFERRED_GENUS_OF());
2750

    
2751
        return inferredGenus;
2752
    }
2753

    
2754
    private Synonym createInferredEpithets(Taxon taxon,
2755
            HashMap<UUID, ZoologicalName> zooHashMap, ZoologicalName taxonName,
2756
            String epithetOfTaxon, String infragenericEpithetOfTaxon,
2757
            String infraspecificEpithetOfTaxon, List<String> taxonNames,
2758
            TaxonNameBase parentName, TaxonBase syn) {
2759

    
2760
        Synonym inferredEpithet;
2761
        TaxonNameBase<?,?> synName;
2762
        ZoologicalName inferredSynName;
2763
        HibernateProxyHelper.deproxy(syn);
2764

    
2765
        // Determine the idInSource
2766
        String idInSourceSyn = getIdInSource(syn);
2767
        String idInSourceTaxon =  getIdInSource(taxon);
2768
        // Determine the sourceReference
2769
        Reference<?> sourceReference = syn.getSec();
2770

    
2771
        if (sourceReference == null){
2772
             logger.warn("The synonym has no sec reference because it is a misapplied name! Take the sec reference of taxon" + taxon.getSec());
2773
             sourceReference = taxon.getSec();
2774
        }
2775

    
2776
        synName = syn.getName();
2777
        ZoologicalName zooSynName = getZoologicalName(synName.getUuid(), zooHashMap);
2778
        String synGenusName = zooSynName.getGenusOrUninomial();
2779
        String synInfraGenericEpithet = null;
2780
        String synSpecificEpithet = null;
2781

    
2782
        if (zooSynName.getInfraGenericEpithet() != null){
2783
            synInfraGenericEpithet = zooSynName.getInfraGenericEpithet();
2784
        }
2785

    
2786
        if (zooSynName.isInfraSpecific()){
2787
            synSpecificEpithet = zooSynName.getSpecificEpithet();
2788
        }
2789

    
2790
                     /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2791
            synonymsGenus.put(synGenusName, idInSource);
2792
        }*/
2793

    
2794
        inferredSynName = ZoologicalName.NewInstance(taxon.getName().getRank());
2795

    
2796
        // DEBUG TODO: for subgenus or subspecies the infrageneric or infraspecific epithet should be used!!!
2797
        if (epithetOfTaxon == null && infragenericEpithetOfTaxon == null && infraspecificEpithetOfTaxon == null) {
2798
            logger.error("This specificEpithet is NULL" + taxon.getTitleCache());
2799
        }
2800
        inferredSynName.setGenusOrUninomial(synGenusName);
2801

    
2802
        if (parentName.isInfraGeneric()){
2803
            inferredSynName.setInfraGenericEpithet(synInfraGenericEpithet);
2804
        }
2805
        if (taxonName.isSpecies()){
2806
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2807
        }else if (taxonName.isInfraSpecific()){
2808
            inferredSynName.setSpecificEpithet(synSpecificEpithet);
2809
            inferredSynName.setInfraSpecificEpithet(infraspecificEpithetOfTaxon);
2810
        }
2811

    
2812
        inferredEpithet = Synonym.NewInstance(inferredSynName, null);
2813

    
2814
        // Set the sourceReference
2815
        inferredEpithet.setSec(sourceReference);
2816

    
2817
        /* Add the original source
2818
        if (idInSource != null) {
2819
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(idInSource, "InferredEpithetOf", syn.getSec(), null);
2820

    
2821
            // Add the citation
2822
            Reference citation = getCitation(syn);
2823
            if (citation != null) {
2824
                originalSource.setCitation(citation);
2825
                inferredEpithet.addSource(originalSource);
2826
            }
2827
        }*/
2828
        String taxonId = idInSourceTaxon+ "; " + idInSourceSyn;
2829

    
2830

    
2831
        IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2832
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2833

    
2834
        inferredEpithet.addSource(originalSource);
2835

    
2836
        originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2837
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2838

    
2839
        inferredSynName.addSource(originalSource);
2840

    
2841

    
2842

    
2843
        taxon.addSynonym(inferredEpithet, SynonymRelationshipType.INFERRED_EPITHET_OF());
2844

    
2845
        return inferredEpithet;
2846
    }
2847

    
2848
    /**
2849
     * Returns an existing ZoologicalName or extends an internal hashmap if it does not exist.
2850
     * Very likely only useful for createInferredSynonyms().
2851
     * @param uuid
2852
     * @param zooHashMap
2853
     * @return
2854
     */
2855
    private ZoologicalName getZoologicalName(UUID uuid, HashMap <UUID, ZoologicalName> zooHashMap) {
2856
        ZoologicalName taxonName =nameDao.findZoologicalNameByUUID(uuid);
2857
        if (taxonName == null) {
2858
            taxonName = zooHashMap.get(uuid);
2859
        }
2860
        return taxonName;
2861
    }
2862

    
2863
    /**
2864
     * Returns the idInSource for a given Synonym.
2865
     * @param syn
2866
     */
2867
    private String getIdInSource(TaxonBase taxonBase) {
2868
        String idInSource = null;
2869
        Set<IdentifiableSource> sources = taxonBase.getSources();
2870
        if (sources.size() == 1) {
2871
            IdentifiableSource source = sources.iterator().next();
2872
            if (source != null) {
2873
                idInSource  = source.getIdInSource();
2874
            }
2875
        } else if (sources.size() > 1) {
2876
            int count = 1;
2877
            idInSource = "";
2878
            for (IdentifiableSource source : sources) {
2879
                idInSource += source.getIdInSource();
2880
                if (count < sources.size()) {
2881
                    idInSource += "; ";
2882
                }
2883
                count++;
2884
            }
2885
        } else if (sources.size() == 0){
2886
            logger.warn("No idInSource for TaxonBase " + taxonBase.getUuid() + " - " + taxonBase.getTitleCache());
2887
        }
2888

    
2889

    
2890
        return idInSource;
2891
    }
2892

    
2893

    
2894
    /**
2895
     * Returns the citation for a given Synonym.
2896
     * @param syn
2897
     */
2898
    private Reference getCitation(Synonym syn) {
2899
        Reference citation = null;
2900
        Set<IdentifiableSource> sources = syn.getSources();
2901
        if (sources.size() == 1) {
2902
            IdentifiableSource source = sources.iterator().next();
2903
            if (source != null) {
2904
                citation = source.getCitation();
2905
            }
2906
        } else if (sources.size() > 1) {
2907
            logger.warn("This Synonym has more than one source: " + syn.getUuid() + " (" + syn.getTitleCache() +")");
2908
        }
2909

    
2910
        return citation;
2911
    }
2912

    
2913
    @Override
2914
    public List<Synonym>  createAllInferredSynonyms(Taxon taxon, Classification tree, boolean doWithMisappliedNames){
2915
        List <Synonym> inferredSynonyms = new ArrayList<Synonym>();
2916

    
2917
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymRelationshipType.INFERRED_EPITHET_OF(), doWithMisappliedNames));
2918
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymRelationshipType.INFERRED_GENUS_OF(), doWithMisappliedNames));
2919
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymRelationshipType.POTENTIAL_COMBINATION_OF(), doWithMisappliedNames));
2920

    
2921
        return inferredSynonyms;
2922
    }
2923

    
2924
    @Override
2925
    public List<Classification> listClassifications(TaxonBase taxonBase, Integer limit, Integer start, List<String> propertyPaths) {
2926

    
2927
        // TODO quickly implemented, create according dao !!!!
2928
        Set<TaxonNode> nodes = new HashSet<TaxonNode>();
2929
        Set<Classification> classifications = new HashSet<Classification>();
2930
        List<Classification> list = new ArrayList<Classification>();
2931

    
2932
        if (taxonBase == null) {
2933
            return list;
2934
        }
2935

    
2936
        taxonBase = load(taxonBase.getUuid());
2937

    
2938
        if (taxonBase instanceof Taxon) {
2939
            nodes.addAll(((Taxon)taxonBase).getTaxonNodes());
2940
        } else {
2941
            for (Taxon taxon : ((Synonym)taxonBase).getAcceptedTaxa() ) {
2942
                nodes.addAll(taxon.getTaxonNodes());
2943
            }
2944
        }
2945
        for (TaxonNode node : nodes) {
2946
            classifications.add(node.getClassification());
2947
        }
2948
        list.addAll(classifications);
2949
        return list;
2950
    }
2951

    
2952
    @Override
2953
    @Transactional(readOnly = false)
2954
    public UpdateResult changeRelatedTaxonToSynonym(UUID fromTaxonUuid,
2955
            UUID toTaxonUuid,
2956
            TaxonRelationshipType oldRelationshipType,
2957
            SynonymRelationshipType synonymRelationshipType) throws DataChangeNoRollbackException {
2958
        UpdateResult result = new UpdateResult();
2959
        Taxon fromTaxon = (Taxon) dao.load(fromTaxonUuid);
2960
        Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
2961
        Synonym synonym = changeRelatedTaxonToSynonym(fromTaxon, toTaxon, oldRelationshipType, synonymRelationshipType);
2962
        result.setCdmEntity(synonym);
2963
        result.addUpdatedObject(fromTaxon);
2964
        result.addUpdatedObject(toTaxon);
2965
        result.addUpdatedObject(synonym);
2966

    
2967
        return result;
2968
    }
2969

    
2970
    @Override
2971
    @Transactional(readOnly = false)
2972
    public Synonym changeRelatedTaxonToSynonym(Taxon fromTaxon, Taxon toTaxon, TaxonRelationshipType oldRelationshipType,
2973
            SynonymRelationshipType synonymRelationshipType) throws DataChangeNoRollbackException {
2974
        // Create new synonym using concept name
2975
                TaxonNameBase<?, ?> synonymName = fromTaxon.getName();
2976
                Synonym synonym = Synonym.NewInstance(synonymName, fromTaxon.getSec());
2977

    
2978
                // Remove concept relation from taxon
2979
                toTaxon.removeTaxon(fromTaxon, oldRelationshipType);
2980

    
2981

    
2982

    
2983

    
2984
                // Create a new synonym for the taxon
2985
                SynonymRelationship synonymRelationship;
2986
                if (synonymRelationshipType != null
2987
                        && synonymRelationshipType.equals(SynonymRelationshipType.HOMOTYPIC_SYNONYM_OF())){
2988
                    synonymRelationship = toTaxon.addHomotypicSynonym(synonym, null, null);
2989
                } else{
2990
                    synonymRelationship = toTaxon.addHeterotypicSynonymName(synonymName);
2991
                }
2992

    
2993
                this.saveOrUpdate(toTaxon);
2994
                //TODO: configurator and classification
2995
                TaxonDeletionConfigurator config = new TaxonDeletionConfigurator();
2996
                config.setDeleteNameIfPossible(false);
2997
                this.deleteTaxon(fromTaxon.getUuid(), config, null);
2998
                return synonymRelationship.getSynonym();
2999

    
3000
    }
3001

    
3002
    @Override
3003
    public DeleteResult isDeletable(TaxonBase taxonBase, DeleteConfiguratorBase config){
3004
        DeleteResult result = new DeleteResult();
3005
        Set<CdmBase> references = commonService.getReferencingObjectsForDeletion(taxonBase);
3006
        if (taxonBase instanceof Taxon){
3007
            TaxonDeletionConfigurator taxonConfig = (TaxonDeletionConfigurator) config;
3008
            result = isDeletableForTaxon(references, taxonConfig);
3009
        }else{
3010
            SynonymDeletionConfigurator synonymConfig = (SynonymDeletionConfigurator) config;
3011
            result = isDeletableForSynonym(references, synonymConfig);
3012
        }
3013
        return result;
3014
    }
3015

    
3016
    private DeleteResult isDeletableForSynonym(Set<CdmBase> references, SynonymDeletionConfigurator config){
3017
        String message;
3018
        DeleteResult result = new DeleteResult();
3019
        for (CdmBase ref: references){
3020
            if (!(ref instanceof SynonymRelationship || ref instanceof Taxon || ref instanceof TaxonNameBase )){
3021
                message = "The Synonym can't be deleted as long as it is referenced by " + ref.getClass().getSimpleName() + " with id "+ ref.getId();
3022
                result.addException(new ReferencedObjectUndeletableException(message));
3023
                result.addRelatedObject(ref);
3024
                result.setAbort();
3025
            }
3026
        }
3027

    
3028
        return result;
3029
    }
3030

    
3031
    private DeleteResult isDeletableForTaxon(Set<CdmBase> references, TaxonDeletionConfigurator config){
3032
        String message = null;
3033
        DeleteResult result = new DeleteResult();
3034
        for (CdmBase ref: references){
3035
            if (!(ref instanceof TaxonNameBase)){
3036
            	message = null;
3037
                if (!config.isDeleteSynonymRelations() && (ref instanceof SynonymRelationship)){
3038
                    message = "The taxon can't be deleted as long as it has synonyms.";
3039

    
3040
                }
3041
                if (!config.isDeleteDescriptions() && (ref instanceof DescriptionBase)){
3042
                    message = "The taxon can't be deleted as long as it has factual data.";
3043

    
3044
                }
3045

    
3046
                if (!config.isDeleteTaxonNodes() && (ref instanceof TaxonNode)){
3047
                    message = "The taxon can't be deleted as long as it belongs to a taxon node.";
3048

    
3049
                }
3050
                if (!config.isDeleteTaxonRelationships() && (ref instanceof TaxonNode)){
3051
                    if (!config.isDeleteMisappliedNamesAndInvalidDesignations() && (((TaxonRelationship)ref).getType().equals(TaxonRelationshipType.MISAPPLIED_NAME_FOR())|| ((TaxonRelationship)ref).getType().equals(TaxonRelationshipType.INVALID_DESIGNATION_FOR()))){
3052
                        message = "The taxon can't be deleted as long as it has misapplied names or invalid designations.";
3053

    
3054
                    } else{
3055
                        message = "The taxon can't be deleted as long as it belongs to a taxon node.";
3056

    
3057
                    }
3058
                }
3059
                if (ref instanceof PolytomousKeyNode){
3060
                    message = "The taxon can't be deleted as long as it is referenced by a polytomous key node.";
3061

    
3062
                }
3063

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

    
3067

    
3068
                }
3069

    
3070

    
3071
               /* //PolytomousKeyNode
3072
                if (referencingObject.isInstanceOf(PolytomousKeyNode.class)){
3073
                    String message = "Taxon" + taxon.getTitleCache() + " can't be deleted as it is used in polytomous key node";
3074
                    return message;
3075
                }*/
3076

    
3077
                //TaxonInteraction
3078
                if (ref.isInstanceOf(TaxonInteraction.class)){
3079
                    message = "Taxon can't be deleted as it is used in taxonInteraction#taxon2";
3080

    
3081
                }
3082

    
3083
              //TaxonInteraction
3084
                if (ref.isInstanceOf(DeterminationEvent.class)){
3085
                    message = "Taxon can't be deleted as it is used in a determination event";
3086

    
3087
                }
3088

    
3089
            }
3090
            if (message != null){
3091
	            result.addException(new ReferencedObjectUndeletableException(message));
3092
	            result.addRelatedObject(ref);
3093
	            result.setAbort();
3094
            }
3095
        }
3096

    
3097
        return result;
3098
    }
3099

    
3100
    @Override
3101
    public IncludedTaxaDTO listIncludedTaxa(UUID taxonUuid, IncludedTaxonConfiguration config) {
3102
        IncludedTaxaDTO result = new IncludedTaxaDTO(taxonUuid);
3103

    
3104
        //preliminary implementation
3105

    
3106
        Set<Taxon> taxa = new HashSet<Taxon>();
3107
        TaxonBase taxonBase = find(taxonUuid);
3108
        if (taxonBase == null){
3109
            return new IncludedTaxaDTO();
3110
        }else if (taxonBase.isInstanceOf(Taxon.class)){
3111
            Taxon taxon = CdmBase.deproxy(taxonBase, Taxon.class);
3112
            taxa.add(taxon);
3113
        }else if (taxonBase.isInstanceOf(Synonym.class)){
3114
            //TODO partial synonyms ??
3115
            //TODO synonyms in general
3116
            Synonym syn = CdmBase.deproxy(taxonBase, Synonym.class);
3117
            taxa.addAll(syn.getAcceptedTaxa());
3118
        }else{
3119
            throw new IllegalArgumentException("Unhandled class " + taxonBase.getClass().getSimpleName());
3120
        }
3121

    
3122
        Set<Taxon> related = makeRelatedIncluded(taxa, result, config);
3123
        int i = 0;
3124
        while((! related.isEmpty()) && i++ < 100){  //to avoid
3125
             related = makeRelatedIncluded(related, result, config);
3126
        }
3127

    
3128
        return result;
3129
    }
3130

    
3131
    /**
3132
     * Computes all children and conceptually congruent and included taxa and adds them to the existingTaxa
3133
     * data structure.
3134
     * @return the set of conceptually related taxa for further use
3135
     */
3136
    /**
3137
     * @param uncheckedTaxa
3138
     * @param existingTaxa
3139
     * @param config
3140
     * @return
3141
     */
3142
    private Set<Taxon> makeRelatedIncluded(Set<Taxon> uncheckedTaxa, IncludedTaxaDTO existingTaxa, IncludedTaxonConfiguration config) {
3143

    
3144
        //children
3145
        Set<TaxonNode> taxonNodes = new HashSet<TaxonNode>();
3146
        for (Taxon taxon: uncheckedTaxa){
3147
            taxonNodes.addAll(taxon.getTaxonNodes());
3148
        }
3149

    
3150
        Set<Taxon> children = new HashSet<Taxon>();
3151
        if (! config.onlyCongruent){
3152
            for (TaxonNode node: taxonNodes){
3153
                List<TaxonNode> childNodes = nodeService.loadChildNodesOfTaxonNode(node, null, true, null);
3154
                for (TaxonNode child : childNodes){
3155
                    children.add(child.getTaxon());
3156
                }
3157
            }
3158
            children.remove(null);  // just to be on the save side
3159
        }
3160

    
3161
        Iterator<Taxon> it = children.iterator();
3162
        while(it.hasNext()){
3163
            UUID uuid = it.next().getUuid();
3164
            if (existingTaxa.contains(uuid)){
3165
                it.remove();
3166
            }else{
3167
                existingTaxa.addIncludedTaxon(uuid, new ArrayList<UUID>(), false);
3168
            }
3169
        }
3170

    
3171
        //concept relations
3172
        Set<Taxon> uncheckedAndChildren = new HashSet<Taxon>(uncheckedTaxa);
3173
        uncheckedAndChildren.addAll(children);
3174

    
3175
        Set<Taxon> relatedTaxa = makeConceptIncludedTaxa(uncheckedAndChildren, existingTaxa, config);
3176

    
3177

    
3178
        Set<Taxon> result = new HashSet<Taxon>(relatedTaxa);
3179
        return result;
3180
    }
3181

    
3182
    /**
3183
     * Computes all conceptually congruent or included taxa and adds them to the existingTaxa data structure.
3184
     * @return the set of these computed taxa
3185
     */
3186
    private Set<Taxon> makeConceptIncludedTaxa(Set<Taxon> unchecked, IncludedTaxaDTO existingTaxa, IncludedTaxonConfiguration config) {
3187
        Set<Taxon> result = new HashSet<Taxon>();
3188

    
3189
        for (Taxon taxon : unchecked){
3190
            Set<TaxonRelationship> fromRelations = taxon.getRelationsFromThisTaxon();
3191
            Set<TaxonRelationship> toRelations = taxon.getRelationsToThisTaxon();
3192

    
3193
            for (TaxonRelationship fromRel : fromRelations){
3194
                if (config.includeDoubtful == false && fromRel.isDoubtful()){
3195
                    continue;
3196
                }
3197
                if (fromRel.getType().equals(TaxonRelationshipType.CONGRUENT_TO()) ||
3198
                        !config.onlyCongruent && fromRel.getType().equals(TaxonRelationshipType.INCLUDES()) ||
3199
                        !config.onlyCongruent && fromRel.getType().equals(TaxonRelationshipType.CONGRUENT_OR_INCLUDES())
3200
                        ){
3201
                    result.add(fromRel.getToTaxon());
3202
                }
3203
            }
3204

    
3205
            for (TaxonRelationship toRel : toRelations){
3206
                if (config.includeDoubtful == false && toRel.isDoubtful()){
3207
                    continue;
3208
                }
3209
                if (toRel.getType().equals(TaxonRelationshipType.CONGRUENT_TO())){
3210
                    result.add(toRel.getFromTaxon());
3211
                }
3212
            }
3213
        }
3214

    
3215
        Iterator<Taxon> it = result.iterator();
3216
        while(it.hasNext()){
3217
            UUID uuid = it.next().getUuid();
3218
            if (existingTaxa.contains(uuid)){
3219
                it.remove();
3220
            }else{
3221
                existingTaxa.addIncludedTaxon(uuid, new ArrayList<UUID>(), false);
3222
            }
3223
        }
3224
        return result;
3225
    }
3226

    
3227
    @Override
3228
    public List<TaxonBase> findTaxaByName(MatchingTaxonConfigurator config){
3229
        List<TaxonBase> taxonList = dao.getTaxaByName(true, false, false, config.getTaxonNameTitle(), null, MatchMode.EXACT, null, 0, 0, config.getPropertyPath());
3230
        return taxonList;
3231
    }
3232

    
3233
	@Override
3234
	@Transactional(readOnly = true)
3235
	public <S extends TaxonBase> Pager<FindByIdentifierDTO<S>> findByIdentifier(
3236
			Class<S> clazz, String identifier, DefinedTerm identifierType, TaxonNode subtreeFilter,
3237
			MatchMode matchmode, boolean includeEntity, Integer pageSize,
3238
			Integer pageNumber,	List<String> propertyPaths) {
3239
		if (subtreeFilter == null){
3240
			return findByIdentifier(clazz, identifier, identifierType, matchmode, includeEntity, pageSize, pageNumber, propertyPaths);
3241
		}
3242

    
3243
		Integer numberOfResults = dao.countByIdentifier(clazz, identifier, identifierType, subtreeFilter, matchmode);
3244
        List<Object[]> daoResults = new ArrayList<Object[]>();
3245
        if(numberOfResults > 0) { // no point checking again
3246
        	daoResults = dao.findByIdentifier(clazz, identifier, identifierType, subtreeFilter,
3247
    				matchmode, includeEntity, pageSize, pageNumber, propertyPaths);
3248
        }
3249

    
3250
        List<FindByIdentifierDTO<S>> result = new ArrayList<FindByIdentifierDTO<S>>();
3251
        for (Object[] daoObj : daoResults){
3252
        	if (includeEntity){
3253
        		result.add(new FindByIdentifierDTO<S>((DefinedTerm)daoObj[0], (String)daoObj[1], (S)daoObj[2]));
3254
        	}else{
3255
        		result.add(new FindByIdentifierDTO<S>((DefinedTerm)daoObj[0], (String)daoObj[1], (UUID)daoObj[2], (String)daoObj[3]));
3256
        	}
3257
        }
3258
		return new DefaultPagerImpl<FindByIdentifierDTO<S>>(pageNumber, numberOfResults, pageSize, result);
3259
	}
3260

    
3261
	@Override
3262
	@Transactional(readOnly = false)
3263
	public UpdateResult moveSynonymToAnotherTaxon(SynonymRelationship oldSynonymRelation, UUID newTaxonUUID, boolean moveHomotypicGroup,
3264
            SynonymRelationshipType newSynonymRelationshipType, Reference reference, String referenceDetail, boolean keepReference) throws HomotypicalGroupChangeException {
3265

    
3266
	    UpdateResult result = new UpdateResult();
3267
		Taxon newTaxon = (Taxon) dao.load(newTaxonUUID);
3268
		result = moveSynonymToAnotherTaxon(oldSynonymRelation, newTaxon, moveHomotypicGroup, newSynonymRelationshipType, reference, referenceDetail, keepReference);
3269

    
3270
		return result;
3271
	}
3272

    
3273
	@Override
3274
	public UpdateResult moveFactualDateToAnotherTaxon(UUID fromTaxonUuid, UUID toTaxonUuid){
3275
		UpdateResult result = new UpdateResult();
3276

    
3277
		Taxon fromTaxon = (Taxon)dao.load(fromTaxonUuid);
3278
		Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
3279
		  for(TaxonDescription description : fromTaxon.getDescriptions()){
3280
              //reload to avoid session conflicts
3281
              description = HibernateProxyHelper.deproxy(description, TaxonDescription.class);
3282

    
3283
              String moveMessage = String.format("Description moved from %s", fromTaxon);
3284
              if(description.isProtectedTitleCache()){
3285
                  String separator = "";
3286
                  if(!StringUtils.isBlank(description.getTitleCache())){
3287
                      separator = " - ";
3288
                  }
3289
                  description.setTitleCache(description.getTitleCache() + separator + moveMessage, true);
3290
              }
3291
              Annotation annotation = Annotation.NewInstance(moveMessage, Language.getDefaultLanguage());
3292
              annotation.setAnnotationType(AnnotationType.TECHNICAL());
3293
              description.addAnnotation(annotation);
3294
              toTaxon.addDescription(description);
3295
              dao.saveOrUpdate(toTaxon);
3296
              dao.saveOrUpdate(fromTaxon);
3297
              result.addUpdatedObject(toTaxon);
3298
              result.addUpdatedObject(fromTaxon);
3299

    
3300
          }
3301

    
3302

    
3303
		return result;
3304
	}
3305

    
3306
	@Override
3307
	public DeleteResult deleteSynonym(UUID synonymUuid, UUID taxonUuid,
3308
			SynonymDeletionConfigurator config) {
3309
		TaxonBase base = this.load(synonymUuid);
3310
		Synonym syn = HibernateProxyHelper.deproxy(base, Synonym.class);
3311
		base = this.load(taxonUuid);
3312
		Taxon taxon = HibernateProxyHelper.deproxy(base, Taxon.class);
3313

    
3314
		return this.deleteSynonym(syn, taxon, config);
3315
	}
3316

    
3317
	@Override
3318
	@Transactional(readOnly = false)
3319
	public UpdateResult swapSynonymAndAcceptedTaxon(UUID synonymUUid,
3320
			UUID acceptedTaxonUuid) {
3321
		TaxonBase base = this.load(synonymUUid);
3322
		Synonym syn = HibernateProxyHelper.deproxy(base, Synonym.class);
3323
		base = this.load(acceptedTaxonUuid);
3324
		Taxon taxon = HibernateProxyHelper.deproxy(base, Taxon.class);
3325

    
3326
		return this.swapSynonymAndAcceptedTaxon(syn, taxon);
3327
	}
3328

    
3329

    
3330

    
3331
}
(90-90/97)