Project

General

Profile

Download (152 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.OriginalSourceType;
79
import eu.etaxonomy.cdm.model.common.RelationshipBase;
80
import eu.etaxonomy.cdm.model.common.RelationshipBase.Direction;
81
import eu.etaxonomy.cdm.model.description.CommonTaxonName;
82
import eu.etaxonomy.cdm.model.description.DescriptionBase;
83
import eu.etaxonomy.cdm.model.description.DescriptionElementBase;
84
import eu.etaxonomy.cdm.model.description.Distribution;
85
import eu.etaxonomy.cdm.model.description.Feature;
86
import eu.etaxonomy.cdm.model.description.IIdentificationKey;
87
import eu.etaxonomy.cdm.model.description.PolytomousKeyNode;
88
import eu.etaxonomy.cdm.model.description.PresenceAbsenceTerm;
89
import eu.etaxonomy.cdm.model.description.SpecimenDescription;
90
import eu.etaxonomy.cdm.model.description.TaxonDescription;
91
import eu.etaxonomy.cdm.model.description.TaxonInteraction;
92
import eu.etaxonomy.cdm.model.description.TaxonNameDescription;
93
import eu.etaxonomy.cdm.model.location.NamedArea;
94
import eu.etaxonomy.cdm.model.media.Media;
95
import eu.etaxonomy.cdm.model.media.MediaRepresentation;
96
import eu.etaxonomy.cdm.model.media.MediaUtils;
97
import eu.etaxonomy.cdm.model.name.HomotypicalGroup;
98
import eu.etaxonomy.cdm.model.name.NameRelationship;
99
import eu.etaxonomy.cdm.model.name.Rank;
100
import eu.etaxonomy.cdm.model.name.TaxonNameBase;
101
import eu.etaxonomy.cdm.model.name.ZoologicalName;
102
import eu.etaxonomy.cdm.model.occurrence.DerivedUnit;
103
import eu.etaxonomy.cdm.model.occurrence.DeterminationEvent;
104
import eu.etaxonomy.cdm.model.occurrence.SpecimenOrObservationBase;
105
import eu.etaxonomy.cdm.model.reference.Reference;
106
import eu.etaxonomy.cdm.model.taxon.Classification;
107
import eu.etaxonomy.cdm.model.taxon.ITaxonTreeNode;
108
import eu.etaxonomy.cdm.model.taxon.Synonym;
109
import eu.etaxonomy.cdm.model.taxon.SynonymRelationship;
110
import eu.etaxonomy.cdm.model.taxon.SynonymRelationshipType;
111
import eu.etaxonomy.cdm.model.taxon.Taxon;
112
import eu.etaxonomy.cdm.model.taxon.TaxonBase;
113
import eu.etaxonomy.cdm.model.taxon.TaxonNode;
114
import eu.etaxonomy.cdm.model.taxon.TaxonRelationship;
115
import eu.etaxonomy.cdm.model.taxon.TaxonRelationshipType;
116
import eu.etaxonomy.cdm.persistence.dao.common.ICdmGenericDao;
117
import eu.etaxonomy.cdm.persistence.dao.common.IOrderedTermVocabularyDao;
118
import eu.etaxonomy.cdm.persistence.dao.initializer.AbstractBeanInitializer;
119
import eu.etaxonomy.cdm.persistence.dao.name.ITaxonNameDao;
120
import eu.etaxonomy.cdm.persistence.dao.occurrence.IOccurrenceDao;
121
import eu.etaxonomy.cdm.persistence.dao.taxon.IClassificationDao;
122
import eu.etaxonomy.cdm.persistence.dao.taxon.ITaxonDao;
123
import eu.etaxonomy.cdm.persistence.dao.taxon.ITaxonNodeDao;
124
import eu.etaxonomy.cdm.persistence.dto.UuidAndTitleCache;
125
import eu.etaxonomy.cdm.persistence.query.MatchMode;
126
import eu.etaxonomy.cdm.persistence.query.OrderHint;
127
import eu.etaxonomy.cdm.persistence.query.OrderHint.SortOrder;
128
import eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy;
129

    
130

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

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

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

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

    
147
    @Autowired
148
    private ITaxonNodeDao taxonNodeDao;
149

    
150
    @Autowired
151
    private ITaxonNameDao nameDao;
152

    
153
    @Autowired
154
    private INameService nameService;
155

    
156
    @Autowired
157
    private IOccurrenceService occurrenceService;
158

    
159
    @Autowired
160
    private ITaxonNodeService nodeService;
161

    
162
    @Autowired
163
    private ICdmGenericDao genericDao;
164

    
165
    @Autowired
166
    private IDescriptionService descriptionService;
167

    
168
    @Autowired
169
    private IOrderedTermVocabularyDao orderedVocabularyDao;
170

    
171
    @Autowired
172
    private IOccurrenceDao occurrenceDao;
173

    
174
    @Autowired
175
    private IClassificationDao classificationDao;
176

    
177
    @Autowired
178
    private AbstractBeanInitializer beanInitializer;
179

    
180
    @Autowired
181
    private ILuceneIndexToolProvider luceneIndexToolProvider;
182

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

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

    
199
    @Override
200
    public List<RelationshipBase> getAllRelationships(int limit, int start){
201
        return dao.getAllRelationships(limit, start);
202
    }
203

    
204
    @Override
205
    @Transactional(readOnly = false)
206
    public UpdateResult swapSynonymAndAcceptedTaxon(Synonym synonym, Taxon acceptedTaxon){
207
    	UpdateResult result = new UpdateResult();
208
        TaxonNameBase<?,?> synonymName = synonym.getName();
209
        synonymName.removeTaxonBase(synonym);
210
        TaxonNameBase<?,?> taxonName = acceptedTaxon.getName();
211
        taxonName.removeTaxonBase(acceptedTaxon);
212

    
213
        synonym.setName(taxonName);
214
        acceptedTaxon.setName(synonymName);
215
        saveOrUpdate(synonym);
216
        saveOrUpdate(acceptedTaxon);
217
        result.addUpdatedObject(acceptedTaxon);
218
        result.addUpdatedObject(synonym);
219
		return result;
220

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

    
226

    
227
    @Override
228
    @Transactional(readOnly = false)
229
    public Taxon changeSynonymToAcceptedTaxon(Synonym synonym, Taxon acceptedTaxon, boolean deleteSynonym, boolean copyCitationInfo, Reference citation, String microCitation) throws HomotypicalGroupChangeException{
230

    
231
        TaxonNameBase<?,?> acceptedName = acceptedTaxon.getName();
232
        TaxonNameBase<?,?> synonymName = synonym.getName();
233
        HomotypicalGroup synonymHomotypicGroup = synonymName.getHomotypicalGroup();
234

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

    
241
        Taxon newAcceptedTaxon = Taxon.NewInstance(synonymName, acceptedTaxon.getSec());
242
        dao.save(newAcceptedTaxon);
243
        SynonymRelationshipType relTypeForGroup = SynonymRelationshipType.HOMOTYPIC_SYNONYM_OF();
244
        List<Synonym> heteroSynonyms = acceptedTaxon.getSynonymsInGroup(synonymHomotypicGroup);
245
        Set<NameRelationship> basionymsAndReplacedSynonyms = synonymHomotypicGroup.getBasionymAndReplacedSynonymRelations();
246

    
247
        for (Synonym heteroSynonym : heteroSynonyms){
248
            if (synonym.equals(heteroSynonym)){
249
                acceptedTaxon.removeSynonym(heteroSynonym, false);
250

    
251
            }else{
252
                //move synonyms in same homotypic group to new accepted taxon
253
                heteroSynonym.replaceAcceptedTaxon(newAcceptedTaxon, relTypeForGroup, copyCitationInfo, citation, microCitation);
254
            }
255
        }
256
        dao.saveOrUpdate(acceptedTaxon);
257
        //synonym.getName().removeTaxonBase(synonym);
258

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

    
267
            } catch (Exception e) {
268
                logger.info("Can't delete old synonym from database");
269
            }
270
        }
271

    
272
        return newAcceptedTaxon;
273
    }
274

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

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

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

    
315
    @Override
316
    @Transactional(readOnly = false)
317
    public Taxon changeSynonymToRelatedTaxon(Synonym synonym, Taxon toTaxon, TaxonRelationshipType taxonRelationshipType, Reference citation, String microcitation){
318

    
319
        // Get name from synonym
320
        if (synonym == null){
321
            return null;
322
        }
323
        TaxonNameBase<?, ?> synonymName = synonym.getName();
324

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

    
331
        // Add taxon relation
332
        fromTaxon.addTaxonRelation(toTaxon, taxonRelationshipType, citation, microcitation);
333

    
334
        // since we are swapping names, we have to detach the name from the synonym completely.
335
        // Otherwise the synonym will still be in the list of typified names.
336
       // synonym.getName().removeTaxonBase(synonym);
337
        this.deleteSynonym(synonym, null);
338

    
339
        return fromTaxon;
340
    }
341

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

    
350

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

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

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

    
366
        //set synonym relationship correctly
367
//			SynonymRelationship relToTaxon = null;
368
        boolean relToTargetTaxonExists = false;
369
        Set<SynonymRelationship> existingRelations = synonym.getSynonymRelations();
370
        for (SynonymRelationship rel : existingRelations){
371
            Taxon acceptedTaxon = rel.getAcceptedTaxon();
372
            boolean isTargetTaxon = acceptedTaxon != null && acceptedTaxon.equals(targetTaxon);
373
            HomotypicalGroup acceptedGroup = acceptedTaxon.getHomotypicGroup();
374
            boolean isHomotypicToTaxon = acceptedGroup.equals(newHomotypicalGroup);
375
            SynonymRelationshipType newRelationType = isHomotypicToTaxon? SynonymRelationshipType.HOMOTYPIC_SYNONYM_OF() : SynonymRelationshipType.HETEROTYPIC_SYNONYM_OF();
376
            rel.setType(newRelationType);
377
            //TODO handle citation and microCitation
378

    
379
            if (isTargetTaxon){
380
                relToTargetTaxonExists = true;
381
            }else{
382
                if (removeFromOtherTaxa){
383
                    acceptedTaxon.removeSynonym(synonym, false);
384
                }else{
385
                    //do nothing
386
                }
387
            }
388
        }
389
        if (targetTaxon != null &&  ! relToTargetTaxonExists ){
390
            Taxon acceptedTaxon = targetTaxon;
391
            HomotypicalGroup acceptedGroup = acceptedTaxon.getHomotypicGroup();
392
            boolean isHomotypicToTaxon = acceptedGroup.equals(newHomotypicalGroup);
393
            SynonymRelationshipType relType = isHomotypicToTaxon? SynonymRelationshipType.HOMOTYPIC_SYNONYM_OF() : SynonymRelationshipType.HETEROTYPIC_SYNONYM_OF();
394
            //TODO handle citation and microCitation
395
            Reference citation = null;
396
            String microCitation = null;
397
            acceptedTaxon.addSynonym(synonym, relType, citation, microCitation);
398
        }
399

    
400
    }
401

    
402
    @Override
403
    @Transactional(readOnly = false)
404
    public void updateTitleCache(Class<? extends TaxonBase> clazz, Integer stepSize, IIdentifiableEntityCacheStrategy<TaxonBase> cacheStrategy, IProgressMonitor monitor) {
405
        if (clazz == null){
406
            clazz = TaxonBase.class;
407
        }
408
        super.updateTitleCacheImpl(clazz, stepSize, cacheStrategy, monitor);
409
    }
410

    
411
    @Override
412
    @Autowired
413
    protected void setDao(ITaxonDao dao) {
414
        this.dao = dao;
415
    }
416

    
417
    @Override
418
    public Pager<TaxonBase> findTaxaByName(Class<? extends TaxonBase> clazz, String uninomial,	String infragenericEpithet, String specificEpithet,	String infraspecificEpithet, String authorship, Rank rank, Integer pageSize,Integer pageNumber) {
419
        if (clazz == null){
420
            clazz = TaxonBase.class;
421
        }
422
        Integer numberOfResults = dao.countTaxaByName(clazz, uninomial, infragenericEpithet, specificEpithet, infraspecificEpithet, rank);
423

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

    
429
        return new DefaultPagerImpl<TaxonBase>(pageNumber, numberOfResults, pageSize, results);
430
    }
431

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

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

    
444
        return results;
445
    }
446

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

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

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

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

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

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

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

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

    
491
    @Override
492
    public List<Taxon> listAcceptedTaxaFor(UUID synonymUuid, UUID classificationUuid, Integer pageSize, Integer pageNumber,
493
            List<OrderHint> orderHints, List<String> propertyPaths){
494
        return pageAcceptedTaxaFor(synonymUuid, classificationUuid, pageSize, pageNumber, orderHints, propertyPaths).getRecords();
495
    }
496

    
497
    @Override
498
    public Pager<Taxon> pageAcceptedTaxaFor(UUID synonymUuid, UUID classificationUuid, Integer pageSize, Integer pageNumber,
499
            List<OrderHint> orderHints, List<String> propertyPaths){
500

    
501
        List<Taxon> list = new ArrayList<Taxon>();
502
        Long count = 0l;
503

    
504
        Synonym synonym = null;
505

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

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

    
523
            }
524
        }
525

    
526
        count = dao.countAcceptedTaxaFor(synonym, classificationFilter) ;
527
        if(count > (pageSize * pageNumber)){
528
            list = dao.listAcceptedTaxaFor(synonym, classificationFilter, pageSize, pageNumber, orderHints, propertyPaths);
529
        }
530

    
531
        return new DefaultPagerImpl<Taxon>(pageNumber, count.intValue(), pageSize, list);
532
    }
533

    
534

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

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

    
545

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

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

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

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

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

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

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

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

    
618
    @Override
619
    public Pager<SynonymRelationship> getSynonyms(Synonym synonym,	SynonymRelationshipType type, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) {
620
        Integer numberOfResults = dao.countSynonyms(synonym, type);
621

    
622
        List<SynonymRelationship> results = new ArrayList<SynonymRelationship>();
623
        if(numberOfResults > 0) { // no point checking again
624
            results = dao.getSynonyms(synonym, type, pageSize, pageNumber, orderHints, propertyPaths);
625
        }
626

    
627
        return new DefaultPagerImpl<SynonymRelationship>(pageNumber, numberOfResults, pageSize, results);
628
    }
629

    
630
    @Override
631
    public List<List<Synonym>> getSynonymsByHomotypicGroup(Taxon taxon, List<String> propertyPaths){
632
         List<List<Synonym>> result = new ArrayList<List<Synonym>>();
633
        Taxon t = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
634

    
635
        //homotypic
636
        result.add(t.getHomotypicSynonymsByHomotypicGroup());
637

    
638
        //heterotypic
639
        List<HomotypicalGroup> homotypicalGroups = t.getHeterotypicSynonymyGroups();
640
        for(HomotypicalGroup homotypicalGroup : homotypicalGroups){
641
            result.add(t.getSynonymsInGroup(homotypicalGroup));
642
        }
643

    
644
        return result;
645

    
646
    }
647

    
648
    @Override
649
    public List<Synonym> getHomotypicSynonymsByHomotypicGroup(Taxon taxon, List<String> propertyPaths){
650
        Taxon t = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
651
        return t.getHomotypicSynonymsByHomotypicGroup();
652
    }
653

    
654
    @Override
655
    public List<List<Synonym>> getHeterotypicSynonymyGroups(Taxon taxon, List<String> propertyPaths){
656
        Taxon t = (Taxon)dao.load(taxon.getUuid(), propertyPaths);
657
        List<HomotypicalGroup> homotypicalGroups = t.getHeterotypicSynonymyGroups();
658
        List<List<Synonym>> heterotypicSynonymyGroups = new ArrayList<List<Synonym>>(homotypicalGroups.size());
659
        for(HomotypicalGroup homotypicalGroup : homotypicalGroups){
660
            heterotypicSynonymyGroups.add(t.getSynonymsInGroup(homotypicalGroup));
661
        }
662
        return heterotypicSynonymyGroups;
663
    }
664

    
665
    @Override
666
    public List<UuidAndTitleCache<IdentifiableEntity>> findTaxaAndNamesForEditor(IFindTaxaAndNamesConfigurator configurator){
667

    
668
        List<UuidAndTitleCache<IdentifiableEntity>> results = new ArrayList<UuidAndTitleCache<IdentifiableEntity>>();
669

    
670

    
671
        if (configurator.isDoSynonyms() || configurator.isDoTaxa() || configurator.isDoNamesWithoutTaxa()){
672
        	results = dao.getTaxaByNameForEditor(configurator.isDoTaxa(), configurator.isDoSynonyms(), configurator.isDoNamesWithoutTaxa(), configurator.isDoMisappliedNames(),configurator.getTitleSearchStringSqlized(), configurator.getClassification(), configurator.getMatchMode(), configurator.getNamedAreas());
673
        }
674
        if (configurator.isDoTaxaByCommonNames()) {
675
            //if(configurator.getPageSize() == null ){
676
                List<UuidAndTitleCache<IdentifiableEntity>> commonNameResults = dao.getTaxaByCommonNameForEditor(configurator.getTitleSearchStringSqlized(), configurator.getClassification(), configurator.getMatchMode(), configurator.getNamedAreas());
677
                if(commonNameResults != null){
678
                    results.addAll(commonNameResults);
679
                }
680
           // }
681
        }
682
        return results;
683
    }
684

    
685
    @Override
686
    public Pager<IdentifiableEntity> findTaxaAndNames(IFindTaxaAndNamesConfigurator configurator) {
687

    
688
        List<IdentifiableEntity> results = new ArrayList<IdentifiableEntity>();
689
        int numberOfResults = 0; // overall number of results (as opposed to number of results per page)
690
        List<TaxonBase> taxa = null;
691

    
692
        // Taxa and synonyms
693
        long numberTaxaResults = 0L;
694

    
695

    
696
        List<String> propertyPath = new ArrayList<String>();
697
        if(configurator.getTaxonPropertyPath() != null){
698
            propertyPath.addAll(configurator.getTaxonPropertyPath());
699
        }
700

    
701

    
702
       if (configurator.isDoMisappliedNames() || configurator.isDoSynonyms() || configurator.isDoTaxa()){
703
            if(configurator.getPageSize() != null){ // no point counting if we need all anyway
704
                numberTaxaResults =
705
                    dao.countTaxaByName(configurator.isDoTaxa(),configurator.isDoSynonyms(), configurator.isDoMisappliedNames(),
706
                        configurator.getTitleSearchStringSqlized(), configurator.getClassification(), configurator.getMatchMode(),
707
                        configurator.getNamedAreas());
708
            }
709

    
710
            if(configurator.getPageSize() == null || numberTaxaResults > configurator.getPageSize() * configurator.getPageNumber()){ // no point checking again if less results
711
                taxa = dao.getTaxaByName(configurator.isDoTaxa(), configurator.isDoSynonyms(),
712
                    configurator.isDoMisappliedNames(), configurator.getTitleSearchStringSqlized(), configurator.getClassification(),
713
                    configurator.getMatchMode(), configurator.getNamedAreas(),
714
                    configurator.getPageSize(), configurator.getPageNumber(), propertyPath);
715
            }
716
       }
717

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

    
720
        if(taxa != null){
721
            results.addAll(taxa);
722
        }
723

    
724
        numberOfResults += numberTaxaResults;
725

    
726
        // Names without taxa
727
        if (configurator.isDoNamesWithoutTaxa()) {
728
            int numberNameResults = 0;
729

    
730
            List<? extends TaxonNameBase<?,?>> names =
731
                nameDao.findByName(configurator.getTitleSearchStringSqlized(), configurator.getMatchMode(),
732
                        configurator.getPageSize(), configurator.getPageNumber(), null, configurator.getTaxonNamePropertyPath());
733
            if (logger.isDebugEnabled()) { logger.debug(names.size() + " matching name(s) found"); }
734
            if (names.size() > 0) {
735
                for (TaxonNameBase<?,?> taxonName : names) {
736
                    if (taxonName.getTaxonBases().size() == 0) {
737
                        results.add(taxonName);
738
                        numberNameResults++;
739
                    }
740
                }
741
                if (logger.isDebugEnabled()) { logger.debug(numberNameResults + " matching name(s) without taxa found"); }
742
                numberOfResults += numberNameResults;
743
            }
744
        }
745

    
746
        // Taxa from common names
747

    
748
        if (configurator.isDoTaxaByCommonNames()) {
749
            taxa = new ArrayList<TaxonBase>();
750
            numberTaxaResults = 0;
751
            if(configurator.getPageSize() != null){// no point counting if we need all anyway
752
                numberTaxaResults = dao.countTaxaByCommonName(configurator.getTitleSearchStringSqlized(), configurator.getClassification(), configurator.getMatchMode(), configurator.getNamedAreas());
753
            }
754
            if(configurator.getPageSize() == null || numberTaxaResults > configurator.getPageSize() * configurator.getPageNumber()){
755
                List<Taxon> commonNameResults = dao.getTaxaByCommonName(configurator.getTitleSearchStringSqlized(), configurator.getClassification(), configurator.getMatchMode(), configurator.getNamedAreas(), configurator.getPageSize(), configurator.getPageNumber(), configurator.getTaxonPropertyPath());
756
                taxa.addAll(commonNameResults);
757
            }
758
            if(taxa != null){
759
                results.addAll(taxa);
760
            }
761
            numberOfResults += numberTaxaResults;
762

    
763
        }
764

    
765
       return new DefaultPagerImpl<IdentifiableEntity>
766
            (configurator.getPageNumber(), numberOfResults, configurator.getPageSize(), results);
767
    }
768

    
769
    public List<UuidAndTitleCache<TaxonBase>> getTaxonUuidAndTitleCache(Integer limit, String pattern){
770
        return dao.getUuidAndTitleCache(limit, pattern);
771
    }
772

    
773
    @Override
774
    public List<MediaRepresentation> getAllMedia(Taxon taxon, int size, int height, int widthOrDuration, String[] mimeTypes){
775
        List<MediaRepresentation> medRep = new ArrayList<MediaRepresentation>();
776
        taxon = (Taxon)dao.load(taxon.getUuid());
777
        Set<TaxonDescription> descriptions = taxon.getDescriptions();
778
        for (TaxonDescription taxDesc: descriptions){
779
            Set<DescriptionElementBase> elements = taxDesc.getElements();
780
            for (DescriptionElementBase descElem: elements){
781
                for(Media media : descElem.getMedia()){
782

    
783
                    //find the best matching representation
784
                    medRep.add(MediaUtils.findBestMatchingRepresentation(media, null, size, height, widthOrDuration, mimeTypes));
785

    
786
                }
787
            }
788
        }
789
        return medRep;
790
    }
791

    
792
    @Override
793
    public List<Media> listTaxonDescriptionMedia(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, boolean limitToGalleries, List<String> propertyPath){
794
        return listMedia(taxon, includeRelationships, limitToGalleries, true, false, false, propertyPath);
795
    }
796

    
797
    @Override
798
    public List<Media> listMedia(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships,
799
            Boolean limitToGalleries, Boolean includeTaxonDescriptions, Boolean includeOccurrences,
800
            Boolean includeTaxonNameDescriptions, List<String> propertyPath) {
801

    
802
    //    logger.setLevel(Level.TRACE);
803
//        Logger.getLogger("org.hibernate.SQL").setLevel(Level.TRACE);
804

    
805
        logger.trace("listMedia() - START");
806

    
807
        Set<Taxon> taxa = new HashSet<Taxon>();
808
        List<Media> taxonMedia = new ArrayList<Media>();
809
        List<Media> nonImageGalleryImages = new ArrayList<Media>();
810

    
811
        if (limitToGalleries == null) {
812
            limitToGalleries = false;
813
        }
814

    
815
        // --- resolve related taxa
816
        if (includeRelationships != null && ! includeRelationships.isEmpty()) {
817
            logger.trace("listMedia() - resolve related taxa");
818
            taxa = listRelatedTaxa(taxon, includeRelationships, null, null, null, null);
819
        }
820

    
821
        taxa.add((Taxon) dao.load(taxon.getUuid()));
822

    
823
        if(includeTaxonDescriptions != null && includeTaxonDescriptions){
824
            logger.trace("listMedia() - includeTaxonDescriptions");
825
            List<TaxonDescription> taxonDescriptions = new ArrayList<TaxonDescription>();
826
            // --- TaxonDescriptions
827
            for (Taxon t : taxa) {
828
                taxonDescriptions.addAll(descriptionService.listTaxonDescriptions(t, null, null, null, null, propertyPath));
829
            }
830
            for (TaxonDescription taxonDescription : taxonDescriptions) {
831
                if (!limitToGalleries || taxonDescription.isImageGallery()) {
832
                    for (DescriptionElementBase element : taxonDescription.getElements()) {
833
                        for (Media media : element.getMedia()) {
834
                            if(taxonDescription.isImageGallery()){
835
                                taxonMedia.add(media);
836
                            }
837
                            else{
838
                                nonImageGalleryImages.add(media);
839
                            }
840
                        }
841
                    }
842
                }
843
            }
844
            //put images from image gallery first (#3242)
845
            taxonMedia.addAll(nonImageGalleryImages);
846
        }
847

    
848

    
849
        if(includeOccurrences != null && includeOccurrences) {
850
            logger.trace("listMedia() - includeOccurrences");
851
            Set<SpecimenOrObservationBase> specimensOrObservations = new HashSet<SpecimenOrObservationBase>();
852
            // --- Specimens
853
            for (Taxon t : taxa) {
854
                specimensOrObservations.addAll(occurrenceDao.listByAssociatedTaxon(null, t, null, null, null, null));
855
            }
856
            for (SpecimenOrObservationBase occurrence : specimensOrObservations) {
857

    
858
//            	direct media removed from specimen #3597
859
//              taxonMedia.addAll(occurrence.getMedia());
860

    
861
                // SpecimenDescriptions
862
                Set<SpecimenDescription> specimenDescriptions = occurrence.getSpecimenDescriptions();
863
                for (DescriptionBase specimenDescription : specimenDescriptions) {
864
                    if (!limitToGalleries || specimenDescription.isImageGallery()) {
865
                        Set<DescriptionElementBase> elements = specimenDescription.getElements();
866
                        for (DescriptionElementBase element : elements) {
867
                            for (Media media : element.getMedia()) {
868
                                taxonMedia.add(media);
869
                            }
870
                        }
871
                    }
872
                }
873

    
874
                if (occurrence.isInstanceOf(DerivedUnit.class)) {
875
                    DerivedUnit derivedUnit = CdmBase.deproxy(occurrence, DerivedUnit.class);
876
                    // Collection
877
                    //TODO why may collections have media attached? #
878
                    if (derivedUnit.getCollection() != null){
879
                        taxonMedia.addAll(derivedUnit.getCollection().getMedia());
880
                    }
881
                }
882
                //media in hierarchy
883
                taxonMedia.addAll(occurrenceService.getMediainHierarchy(occurrence, null, null, propertyPath).getRecords());
884
            }
885
        }
886

    
887
        if(includeTaxonNameDescriptions != null && includeTaxonNameDescriptions) {
888
            logger.trace("listMedia() - includeTaxonNameDescriptions");
889
            // --- TaxonNameDescription
890
            Set<TaxonNameDescription> nameDescriptions = new HashSet<TaxonNameDescription>();
891
            for (Taxon t : taxa) {
892
                nameDescriptions .addAll(t.getName().getDescriptions());
893
            }
894
            for(TaxonNameDescription nameDescription: nameDescriptions){
895
                if (!limitToGalleries || nameDescription.isImageGallery()) {
896
                    Set<DescriptionElementBase> elements = nameDescription.getElements();
897
                    for (DescriptionElementBase element : elements) {
898
                        for (Media media : element.getMedia()) {
899
                            taxonMedia.add(media);
900
                        }
901
                    }
902
                }
903
            }
904
        }
905

    
906

    
907
        logger.trace("listMedia() - initialize");
908
        beanInitializer.initializeAll(taxonMedia, propertyPath);
909

    
910
        logger.trace("listMedia() - END");
911

    
912
        return taxonMedia;
913
    }
914

    
915
    @Override
916
    public List<TaxonBase> findTaxaByID(Set<Integer> listOfIDs) {
917
        return this.dao.loadList(listOfIDs, null);
918
    }
919

    
920
    @Override
921
    public TaxonBase findTaxonByUuid(UUID uuid, List<String> propertyPaths){
922
        return this.dao.findByUuid(uuid, null ,propertyPaths);
923
    }
924

    
925
    @Override
926
    public int countAllRelationships() {
927
        return this.dao.countAllRelationships();
928
    }
929

    
930
    @Override
931
    public List<TaxonNameBase> findIdenticalTaxonNames(List<String> propertyPath) {
932
        return this.dao.findIdenticalTaxonNames(propertyPath);
933
    }
934

    
935
    @Override
936
    @Transactional(readOnly = false)
937
    public DeleteResult deleteTaxon(UUID taxonUUID, TaxonDeletionConfigurator config, UUID classificationUuid)  {
938

    
939
    	if (config == null){
940
            config = new TaxonDeletionConfigurator();
941
        }
942
    	Taxon taxon = (Taxon)dao.load(taxonUUID);
943
    	DeleteResult result = new DeleteResult();
944
    	if (taxon == null){
945
    	    result.setAbort();
946
    	    result.addException(new Exception ("The taxon was already deleted."));
947
    	    return result;
948
    	}
949
    	taxon = HibernateProxyHelper.deproxy(taxon);
950
    	Classification classification = HibernateProxyHelper.deproxy(classificationDao.load(classificationUuid), Classification.class);
951
        result = isDeletable(taxon, config);
952

    
953
        if (result.isOk()){
954
            // --- DeleteSynonymRelations
955
            if (config.isDeleteSynonymRelations()){
956
                boolean removeSynonymNameFromHomotypicalGroup = false;
957
                // use tmp Set to avoid concurrent modification
958
                Set<SynonymRelationship> synRelsToDelete = new HashSet<SynonymRelationship>();
959
                synRelsToDelete.addAll(taxon.getSynonymRelations());
960
                for (SynonymRelationship synRel : synRelsToDelete){
961
                    Synonym synonym = synRel.getSynonym();
962
                    // taxon.removeSynonymRelation will set the accepted taxon and the synonym to NULL
963
                    // this will cause hibernate to delete the relationship since
964
                    // the SynonymRelationship field on both is annotated with removeOrphan
965
                    // so no further explicit deleting of the relationship should be done here
966
                    taxon.removeSynonymRelation(synRel, removeSynonymNameFromHomotypicalGroup);
967

    
968
                    // --- DeleteSynonymsIfPossible
969
                    if (config.isDeleteSynonymsIfPossible()){
970
                        //TODO which value
971
                        boolean newHomotypicGroupIfNeeded = true;
972
                        SynonymDeletionConfigurator synConfig = new SynonymDeletionConfigurator();
973
                        deleteSynonym(synonym, taxon, synConfig);
974
                    }
975
                    // relationship will be deleted by hibernate automatically,
976
                    // see comment above and http://dev.e-taxonomy.eu/trac/ticket/3797
977

    
978
                }
979
            }
980

    
981
            // --- DeleteTaxonRelationships
982
            if (! config.isDeleteTaxonRelationships()){
983
                if (taxon.getTaxonRelations().size() > 0){
984
                    result.setAbort();
985
                    result.addException(new Exception("Taxon can't be deleted as it is related to another taxon. " +
986
                            "Remove taxon from all relations to other taxa prior to deletion."));
987

    
988
                }
989
            } else{
990
                TaxonDeletionConfigurator configRelTaxon = new TaxonDeletionConfigurator();
991
                configRelTaxon.setDeleteTaxonNodes(false);
992
                configRelTaxon.setDeleteConceptRelationships(true);
993

    
994
                for (TaxonRelationship taxRel: taxon.getTaxonRelations()){
995
                    if (config.isDeleteMisappliedNamesAndInvalidDesignations()){
996
                        if (taxRel.getType().equals(TaxonRelationshipType.MISAPPLIED_NAME_FOR()) || taxRel.getType().equals(TaxonRelationshipType.INVALID_DESIGNATION_FOR())){
997
                            if (taxon.equals(taxRel.getToTaxon())){
998
                                this.deleteTaxon(taxRel.getFromTaxon().getUuid(), config, classificationUuid);
999
                            }
1000
                        }
1001
                    } else if (config.isDeleteConceptRelationships() && taxRel.getType().isConceptRelationship()){
1002

    
1003
                        if (taxon.equals(taxRel.getToTaxon()) && isDeletable(taxRel.getFromTaxon(), configRelTaxon).isOk()){
1004
                            this.deleteTaxon(taxRel.getFromTaxon().getUuid(), configRelTaxon, classificationUuid);
1005
                        }else if (isDeletable(taxRel.getToTaxon(), configRelTaxon).isOk()){
1006
                            this.deleteTaxon(taxRel.getToTaxon().getUuid(), configRelTaxon, classificationUuid);
1007
                        }
1008
                    }
1009
                    taxon.removeTaxonRelation(taxRel);
1010

    
1011
                }
1012
            }
1013

    
1014
            //    	TaxonDescription
1015
            if (config.isDeleteDescriptions()){
1016
                Set<TaxonDescription> descriptions = taxon.getDescriptions();
1017
                List<TaxonDescription> removeDescriptions = new ArrayList<TaxonDescription>();
1018
                for (TaxonDescription desc: descriptions){
1019
                    //TODO use description delete configurator ?
1020
                    //FIXME check if description is ALWAYS deletable
1021
                    if (desc.getDescribedSpecimenOrObservation() != null){
1022
                        result.setAbort();
1023
                        result.addException(new Exception("Taxon can't be deleted as it is used in a TaxonDescription" +
1024
                                " which also describes specimens or observations"));
1025
                        break;
1026
                    }
1027
                    removeDescriptions.add(desc);
1028

    
1029

    
1030
                }
1031
                if (result.isOk()){
1032
                    for (TaxonDescription desc: removeDescriptions){
1033
                        taxon.removeDescription(desc);
1034
                        descriptionService.delete(desc);
1035
                    }
1036
                } else {
1037
                    return result;
1038
                }
1039
            }
1040

    
1041

    
1042
         if (! config.isDeleteTaxonNodes() || (!config.isDeleteInAllClassifications() && classification == null && taxon.getTaxonNodes().size() > 1)){
1043
                //if (taxon.getTaxonNodes().size() > 0){
1044
                 result.addException(new Exception( "Taxon can't be deleted as it is used in more than one classification."));
1045
                   // throw new ReferencedObjectUndeletableException(message);
1046
                //}
1047
         }else{
1048
                if (taxon.getTaxonNodes().size() != 0){
1049
                    Set<TaxonNode> nodes = taxon.getTaxonNodes();
1050
                    Iterator<TaxonNode> iterator = nodes.iterator();
1051
                    TaxonNode node = null;
1052
                    boolean deleteChildren;
1053
                    if (config.getTaxonNodeConfig().getChildHandling().equals(ChildHandling.DELETE)){
1054
                        deleteChildren = true;
1055
                    }else {
1056
                        deleteChildren = false;
1057
                    }
1058
                    boolean success = true;
1059
                    if (!config.isDeleteInAllClassifications() && !(classification == null)){
1060
                        while (iterator.hasNext()){
1061
                            node = iterator.next();
1062
                            if (node.getClassification().equals(classification)){
1063
                                break;
1064
                            }
1065
                            node = null;
1066
                        }
1067
                        if (node != null){
1068
                            HibernateProxyHelper.deproxy(node, TaxonNode.class);
1069
                            success =taxon.removeTaxonNode(node, deleteChildren);
1070
                            nodeService.delete(node);
1071
                        } else {
1072
                        	result.setError();
1073
                        	result.addException(new Exception("The taxon can not be deleted because it is not used in defined classification."));
1074
                        }
1075
                    } else if (config.isDeleteInAllClassifications()){
1076
                        List<TaxonNode> nodesList = new ArrayList<TaxonNode>();
1077
                        nodesList.addAll(taxon.getTaxonNodes());
1078

    
1079
                            for (ITaxonTreeNode treeNode: nodesList){
1080
                                TaxonNode taxonNode = (TaxonNode) treeNode;
1081
                                if(!deleteChildren){
1082
                                    Object[] childNodes = taxonNode.getChildNodes().toArray();
1083
                                    for (Object childNode: childNodes){
1084
                                        TaxonNode childNodeCast = (TaxonNode) childNode;
1085
                                        taxonNode.getParent().addChildNode(childNodeCast, childNodeCast.getReference(), childNodeCast.getMicroReference());
1086
                                    }
1087

    
1088
                                    //taxon.removeTaxonNode(taxonNode);
1089
                                }
1090
                            }
1091
                        config.getTaxonNodeConfig().setDeleteElement(false);
1092
                        DeleteResult resultNodes = nodeService.deleteTaxonNodes(nodesList, config);
1093
                        if (!resultNodes.isOk()){
1094
                        	result.addExceptions(resultNodes.getExceptions());
1095
                        	result.setStatus(resultNodes.getStatus());
1096
                        } else {
1097
                            result.addUpdatedObjects(resultNodes.getUpdatedObjects());
1098
                        }
1099
                    }
1100
                    if (!success){
1101
                        result.setError();
1102
                        result.addException(new Exception("The taxon can not be deleted because the taxon node can not be removed."));
1103
                    }
1104
                }
1105
            }
1106

    
1107
            //TaxonNameBase
1108
            if (config.isDeleteNameIfPossible() && result.isOk()){
1109

    
1110

    
1111
                    //TaxonNameBase name = nameService.find(taxon.getName().getUuid());
1112
                    TaxonNameBase name = HibernateProxyHelper.deproxy(taxon.getName());
1113
                    //check whether taxon will be deleted or not
1114

    
1115
                    if ((taxon.getTaxonNodes() == null || taxon.getTaxonNodes().size()== 0) && name != null ){
1116

    
1117
                        //name.removeTaxonBase(taxon);
1118
                        //nameService.saveOrUpdate(name);
1119
                        taxon.setName(null);
1120
                        //dao.delete(taxon);
1121
                        DeleteResult nameResult = new DeleteResult();
1122

    
1123
                        //remove name if possible (and required)
1124
                        if (name != null && config.isDeleteNameIfPossible()){
1125
                        	nameResult = nameService.delete(name.getUuid(), config.getNameDeletionConfig());
1126
                        }
1127

    
1128
                        if (nameResult.isError() || nameResult.isAbort()){
1129
                        	//result.setError();
1130
                        	result.addRelatedObject(name);
1131
                        	result.addExceptions(nameResult.getExceptions());
1132
                        }
1133

    
1134
                    }
1135

    
1136
            }else {
1137
                taxon.setName(null);
1138
            }
1139

    
1140

    
1141
            if ((taxon.getTaxonNodes() == null || taxon.getTaxonNodes().size()== 0)  && result.isOk()){
1142
            	try{
1143
            		UUID uuid = dao.delete(taxon);
1144

    
1145
            	}catch(Exception e){
1146
            		result.addException(e);
1147
            		result.setError();
1148

    
1149
            	}
1150
            } else {
1151
            	result.setError();
1152
            	result.addException(new Exception("The Taxon can't be deleted because it is used in a classification."));
1153

    
1154
            }
1155
        }
1156

    
1157
        return result;
1158

    
1159
    }
1160

    
1161
    private String checkForReferences(Taxon taxon){
1162
        Set<CdmBase> referencingObjects = genericDao.getReferencingObjects(taxon);
1163
        for (CdmBase referencingObject : referencingObjects){
1164
            //IIdentificationKeys (Media, Polytomous, MultiAccess)
1165
            if (HibernateProxyHelper.isInstanceOf(referencingObject, IIdentificationKey.class)){
1166
                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";
1167

    
1168
                return message;
1169
            }
1170

    
1171

    
1172
           /* //PolytomousKeyNode
1173
            if (referencingObject.isInstanceOf(PolytomousKeyNode.class)){
1174
                String message = "Taxon" + taxon.getTitleCache() + " can't be deleted as it is used in polytomous key node";
1175
                return message;
1176
            }*/
1177

    
1178
            //TaxonInteraction
1179
            if (referencingObject.isInstanceOf(TaxonInteraction.class)){
1180
                String message = "Taxon can't be deleted as it is used in taxonInteraction#taxon2";
1181
                return message;
1182
            }
1183

    
1184
          //TaxonInteraction
1185
            if (referencingObject.isInstanceOf(DeterminationEvent.class)){
1186
                String message = "Taxon can't be deleted as it is used in a determination event";
1187
                return message;
1188
            }
1189

    
1190
        }
1191

    
1192
        referencingObjects = null;
1193
        return null;
1194
    }
1195

    
1196
    private boolean checkForPolytomousKeys(Taxon taxon){
1197
        boolean result = false;
1198
        List<CdmBase> list = genericDao.getCdmBasesByFieldAndClass(PolytomousKeyNode.class, "taxon", taxon);
1199
        if (!list.isEmpty()) {
1200
            result = true;
1201
        }
1202
        return result;
1203
    }
1204

    
1205
    @Override
1206
    @Transactional(readOnly = false)
1207
    public DeleteResult delete(UUID synUUID){
1208
    	DeleteResult result = new DeleteResult();
1209
    	Synonym syn = (Synonym)dao.load(synUUID);
1210

    
1211
        return this.deleteSynonym(syn, null);
1212
    }
1213

    
1214

    
1215
    @Override
1216
    @Transactional(readOnly = false)
1217
    public DeleteResult deleteSynonym(Synonym synonym, SynonymDeletionConfigurator config) {
1218
        return deleteSynonym(synonym, null, config);
1219

    
1220
    }
1221

    
1222
    @Override
1223
    @Transactional(readOnly = false)
1224
    public DeleteResult deleteSynonym(UUID synonymUuid, SynonymDeletionConfigurator config) {
1225
        return deleteSynonym((Synonym)dao.load(synonymUuid), config);
1226

    
1227
    }
1228

    
1229
    @Transactional(readOnly = false)
1230
    @Override
1231
    public DeleteResult deleteSynonym(Synonym synonym, Taxon taxon, SynonymDeletionConfigurator config) {
1232
        DeleteResult result = new DeleteResult();
1233
    	if (synonym == null){
1234
    		result.setAbort();
1235
    		result.addException(new Exception("The synonym was already deleted."));
1236
    		return result;
1237
        }
1238

    
1239
        if (config == null){
1240
            config = new SynonymDeletionConfigurator();
1241
        }
1242

    
1243
        result = isDeletable(synonym, config);
1244

    
1245

    
1246
        if (result.isOk()){
1247

    
1248
            synonym = HibernateProxyHelper.deproxy(this.load(synonym.getUuid()), Synonym.class);
1249

    
1250
            //remove synonymRelationship
1251
            Set<Taxon> taxonSet = new HashSet<Taxon>();
1252
            if (taxon != null){
1253
                taxonSet.add(taxon);
1254
            }else{
1255
                taxonSet.addAll(synonym.getAcceptedTaxa());
1256
            }
1257
            for (Taxon relatedTaxon : taxonSet){
1258
            	relatedTaxon = HibernateProxyHelper.deproxy(relatedTaxon, Taxon.class);
1259
                relatedTaxon.removeSynonym(synonym, false);
1260
                this.saveOrUpdate(relatedTaxon);
1261
            }
1262
            this.saveOrUpdate(synonym);
1263

    
1264
            //TODO remove name from homotypical group?
1265

    
1266
            //remove synonym (if necessary)
1267

    
1268
            result.addUpdatedObject(taxon);
1269
            if (synonym.getSynonymRelations().isEmpty()){
1270
                TaxonNameBase<?,?> name = synonym.getName();
1271
                synonym.setName(null);
1272
                dao.delete(synonym);
1273

    
1274
                //remove name if possible (and required)
1275
                if (name != null && config.isDeleteNameIfPossible()){
1276

    
1277
                        DeleteResult nameDeleteresult = nameService.delete(name.getUuid(), config.getNameDeletionConfig());
1278
                        if (nameDeleteresult.isAbort()){
1279
                        	result.addExceptions(nameDeleteresult.getExceptions());
1280
                        	result.addRelatedObject(name);
1281
                        }
1282

    
1283
                }
1284

    
1285
            }else {
1286
            	result.setError();
1287
            	result.addException(new ReferencedObjectUndeletableException("Synonym can not be deleted it is used in an other synonymRelationship."));
1288
                return result;
1289
            }
1290

    
1291

    
1292
        }
1293
        return result;
1294
//        else{
1295
//        	List<Exception> exceptions = new ArrayList<Exception>();
1296
//        	for (String message :messages){
1297
//        		exceptions.add(new ReferencedObjectUndeletableException(message));
1298
//        	}
1299
//        	result.setError();
1300
//        	result.addExceptions(exceptions);
1301
//            return result;
1302
//        }
1303

    
1304

    
1305
    }
1306

    
1307
    @Override
1308
    public List<TaxonNameBase> findIdenticalTaxonNameIds(List<String> propertyPath) {
1309

    
1310
        return this.dao.findIdenticalNamesNew(propertyPath);
1311
    }
1312

    
1313
    @Override
1314
    public String getPhylumName(TaxonNameBase name){
1315
        return this.dao.getPhylumName(name);
1316
    }
1317

    
1318
    @Override
1319
    public long deleteSynonymRelationships(Synonym syn) {
1320
        return dao.deleteSynonymRelationships(syn, null);
1321
    }
1322

    
1323
    @Override
1324
    public List<SynonymRelationship> listSynonymRelationships(
1325
            TaxonBase taxonBase, SynonymRelationshipType type, Integer pageSize, Integer pageNumber,
1326
            List<OrderHint> orderHints, List<String> propertyPaths, Direction direction) {
1327
        Integer numberOfResults = dao.countSynonymRelationships(taxonBase, type, direction);
1328

    
1329
        List<SynonymRelationship> results = new ArrayList<SynonymRelationship>();
1330
        if(numberOfResults > 0) { // no point checking again
1331
            results = dao.getSynonymRelationships(taxonBase, type, pageSize, pageNumber, orderHints, propertyPaths, direction);
1332
        }
1333
        return results;
1334
    }
1335

    
1336
    @Override
1337
    public Taxon findBestMatchingTaxon(String taxonName) {
1338
        MatchingTaxonConfigurator config = MatchingTaxonConfigurator.NewInstance();
1339
        config.setTaxonNameTitle(taxonName);
1340
        return findBestMatchingTaxon(config);
1341
    }
1342

    
1343
    @Override
1344
    public Taxon findBestMatchingTaxon(MatchingTaxonConfigurator config) {
1345

    
1346
        Taxon bestCandidate = null;
1347
        try{
1348
            // 1. search for acceptet taxa
1349
            List<TaxonBase> taxonList = dao.findByNameTitleCache(true, false, config.getTaxonNameTitle(), null, MatchMode.EXACT, null, 0, null, null);
1350
            boolean bestCandidateMatchesSecUuid = false;
1351
            boolean bestCandidateIsInClassification = false;
1352
            int countEqualCandidates = 0;
1353
            for(TaxonBase taxonBaseCandidate : taxonList){
1354
                if(taxonBaseCandidate instanceof Taxon){
1355
                    Taxon newCanditate = CdmBase.deproxy(taxonBaseCandidate, Taxon.class);
1356
                    boolean newCandidateMatchesSecUuid = isMatchesSecUuid(newCanditate, config);
1357
                    if (! newCandidateMatchesSecUuid && config.isOnlyMatchingSecUuid() ){
1358
                        continue;
1359
                    }else if(newCandidateMatchesSecUuid && ! bestCandidateMatchesSecUuid){
1360
                        bestCandidate = newCanditate;
1361
                        countEqualCandidates = 1;
1362
                        bestCandidateMatchesSecUuid = true;
1363
                        continue;
1364
                    }
1365

    
1366
                    boolean newCandidateInClassification = isInClassification(newCanditate, config);
1367
                    if (! newCandidateInClassification && config.isOnlyMatchingClassificationUuid()){
1368
                        continue;
1369
                    }else if (newCandidateInClassification && ! bestCandidateIsInClassification){
1370
                        bestCandidate = newCanditate;
1371
                        countEqualCandidates = 1;
1372
                        bestCandidateIsInClassification = true;
1373
                        continue;
1374
                    }
1375
                    if (bestCandidate == null){
1376
                        bestCandidate = newCanditate;
1377
                        countEqualCandidates = 1;
1378
                        continue;
1379
                    }
1380

    
1381
                }else{  //not Taxon.class
1382
                    continue;
1383
                }
1384
                countEqualCandidates++;
1385

    
1386
            }
1387
            if (bestCandidate != null){
1388
                if(countEqualCandidates > 1){
1389
                    logger.info(countEqualCandidates + " equally matching TaxonBases found, using first accepted Taxon: " + bestCandidate.getTitleCache());
1390
                    return bestCandidate;
1391
                } else {
1392
                    logger.info("using accepted Taxon: " + bestCandidate.getTitleCache());
1393
                    return bestCandidate;
1394
                }
1395
            }
1396

    
1397

    
1398
            // 2. search for synonyms
1399
            if (config.isIncludeSynonyms()){
1400
                List<TaxonBase> synonymList = dao.findByNameTitleCache(false, true, config.getTaxonNameTitle(), null, MatchMode.EXACT, null, 0, null, null);
1401
                for(TaxonBase taxonBase : synonymList){
1402
                    if(taxonBase instanceof Synonym){
1403
                        Synonym synonym = CdmBase.deproxy(taxonBase, Synonym.class);
1404
                        Set<Taxon> acceptetdCandidates = synonym.getAcceptedTaxa();
1405
                        if(!acceptetdCandidates.isEmpty()){
1406
                            bestCandidate = acceptetdCandidates.iterator().next();
1407
                            if(acceptetdCandidates.size() == 1){
1408
                                logger.info(acceptetdCandidates.size() + " Accepted taxa found for synonym " + taxonBase.getTitleCache() + ", using first one: " + bestCandidate.getTitleCache());
1409
                                return bestCandidate;
1410
                            } else {
1411
                                logger.info("using accepted Taxon " +  bestCandidate.getTitleCache() + "for synonym " + taxonBase.getTitleCache());
1412
                                return bestCandidate;
1413
                            }
1414
                            //TODO extend method: search using treeUUID, using SecUUID, first find accepted then include synonyms until a matching taxon is found
1415
                        }
1416
                    }
1417
                }
1418
            }
1419

    
1420
        } catch (Exception e){
1421
            logger.error(e);
1422
            e.printStackTrace();
1423
        }
1424

    
1425
        return bestCandidate;
1426
    }
1427

    
1428
    private boolean isInClassification(Taxon taxon, MatchingTaxonConfigurator config) {
1429
        UUID configClassificationUuid = config.getClassificationUuid();
1430
        if (configClassificationUuid == null){
1431
            return false;
1432
        }
1433
        for (TaxonNode node : taxon.getTaxonNodes()){
1434
            UUID classUuid = node.getClassification().getUuid();
1435
            if (configClassificationUuid.equals(classUuid)){
1436
                return true;
1437
            }
1438
        }
1439
        return false;
1440
    }
1441

    
1442
    private boolean isMatchesSecUuid(Taxon taxon, MatchingTaxonConfigurator config) {
1443
        UUID configSecUuid = config.getSecUuid();
1444
        if (configSecUuid == null){
1445
            return false;
1446
        }
1447
        UUID taxonSecUuid = (taxon.getSec() == null)? null : taxon.getSec().getUuid();
1448
        return configSecUuid.equals(taxonSecUuid);
1449
    }
1450

    
1451
    @Override
1452
    public Synonym findBestMatchingSynonym(String taxonName) {
1453
        List<TaxonBase> synonymList = dao.findByNameTitleCache(false, true, taxonName, null, MatchMode.EXACT, null, 0, null, null);
1454
        if(! synonymList.isEmpty()){
1455
            Synonym result = CdmBase.deproxy(synonymList.iterator().next(), Synonym.class);
1456
            if(synonymList.size() == 1){
1457
                logger.info(synonymList.size() + " Synonym found " + result.getTitleCache() );
1458
                return result;
1459
            } else {
1460
                logger.info("Several matching synonyms found. Using first: " +  result.getTitleCache());
1461
                return result;
1462
            }
1463
        }
1464
        return null;
1465
    }
1466

    
1467
    @Override
1468
    @Transactional(readOnly = false)
1469
    public UpdateResult moveSynonymToAnotherTaxon(SynonymRelationship oldSynonymRelation,
1470
            Taxon newTaxon,
1471
            boolean moveHomotypicGroup,
1472
            SynonymRelationshipType newSynonymRelationshipType,
1473
            Reference reference,
1474
            String referenceDetail,
1475
            boolean keepReference) throws HomotypicalGroupChangeException {
1476

    
1477
        Synonym synonym = (Synonym) dao.load(oldSynonymRelation.getSynonym().getUuid());
1478
        Taxon fromTaxon = (Taxon) dao.load(oldSynonymRelation.getAcceptedTaxon().getUuid());
1479
        //TODO what if there is no name ?? Concepts may be cached (e.g. via TCS import)
1480
        TaxonNameBase<?,?> synonymName = synonym.getName();
1481
        TaxonNameBase<?,?> fromTaxonName = fromTaxon.getName();
1482
        //set default relationship type
1483
        if (newSynonymRelationshipType == null){
1484
            newSynonymRelationshipType = SynonymRelationshipType.HETEROTYPIC_SYNONYM_OF();
1485
        }
1486
        boolean newRelTypeIsHomotypic = newSynonymRelationshipType.equals(SynonymRelationshipType.HOMOTYPIC_SYNONYM_OF());
1487

    
1488
        HomotypicalGroup homotypicGroup = synonymName.getHomotypicalGroup();
1489
        int hgSize = homotypicGroup.getTypifiedNames().size();
1490
        boolean isSingleInGroup = !(hgSize > 1);
1491

    
1492
        if (! isSingleInGroup){
1493
            boolean isHomotypicToAccepted = synonymName.isHomotypic(fromTaxonName);
1494
            boolean hasHomotypicSynonymRelatives = isHomotypicToAccepted ? hgSize > 2 : hgSize > 1;
1495
            if (isHomotypicToAccepted){
1496
                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.";
1497
                String homotypicRelatives = hasHomotypicSynonymRelatives ? " and other synonym(s)":"";
1498
                message = String.format(message, homotypicRelatives);
1499
                throw new HomotypicalGroupChangeException(message);
1500
            }
1501
            if (! moveHomotypicGroup){
1502
                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.";
1503
                throw new HomotypicalGroupChangeException(message);
1504
            }
1505
        }else{
1506
            moveHomotypicGroup = true;  //single synonym always allows to moveCompleteGroup
1507
        }
1508
//        Assert.assertTrue("Synonym can only be moved with complete homotypic group", moveHomotypicGroup);
1509

    
1510
        UpdateResult result = new UpdateResult();
1511
        //move all synonyms to new taxon
1512
        List<Synonym> homotypicSynonyms = fromTaxon.getSynonymsInGroup(homotypicGroup);
1513
        for (Synonym syn: homotypicSynonyms){
1514
            Set<SynonymRelationship> synRelations = syn.getSynonymRelations();
1515
            for (SynonymRelationship synRelation : synRelations){
1516
                if (fromTaxon.equals(synRelation.getAcceptedTaxon())){
1517
                    Reference newReference = reference;
1518
                    if (newReference == null && keepReference){
1519
                        newReference = synRelation.getCitation();
1520
                    }
1521
                    String newRefDetail = referenceDetail;
1522
                    if (newRefDetail == null && keepReference){
1523
                        newRefDetail = synRelation.getCitationMicroReference();
1524
                    }
1525
                    newTaxon = HibernateProxyHelper.deproxy(newTaxon, Taxon.class);
1526
                    fromTaxon = HibernateProxyHelper.deproxy(fromTaxon, Taxon.class);
1527
                    SynonymRelationship newSynRelation = newTaxon.addSynonym(syn, newSynonymRelationshipType, newReference, newRefDetail);
1528
                    fromTaxon.removeSynonymRelation(synRelation, false);
1529
//
1530
                    //change homotypic group of synonym if relType is 'homotypic'
1531
//                	if (newRelTypeIsHomotypic){
1532
//                		newTaxon.getName().getHomotypicalGroup().addTypifiedName(syn.getName());
1533
//                	}
1534
                    //set result
1535
                    if (!synRelation.equals(oldSynonymRelation)){
1536
                        result.setError();
1537
                    }
1538
                }
1539
            }
1540

    
1541
        }
1542
        result.addUpdatedObject(fromTaxon);
1543
        result.addUpdatedObject(newTaxon);
1544
        saveOrUpdate(fromTaxon);
1545
        saveOrUpdate(newTaxon);
1546
        //Assert that there is a result
1547
        if (result == null){
1548
            String message = "Old synonym relation could not be transformed into new relation. This should not happen.";
1549
            throw new IllegalStateException(message);
1550
        }
1551
        return result;
1552
    }
1553

    
1554
    @Override
1555
    public List<UuidAndTitleCache<TaxonBase>> getUuidAndTitleCacheTaxon(Integer limit, String pattern) {
1556
        return dao.getUuidAndTitleCacheTaxon(limit, pattern);
1557
    }
1558

    
1559
    @Override
1560
    public List<UuidAndTitleCache<TaxonBase>> getUuidAndTitleCacheSynonym(Integer limit, String pattern) {
1561
        return dao.getUuidAndTitleCacheSynonym(limit, pattern);
1562
    }
1563

    
1564
    @Override
1565
    public Pager<SearchResult<TaxonBase>> findByFullText(
1566
            Class<? extends TaxonBase> clazz, String queryString,
1567
            Classification classification, List<Language> languages,
1568
            boolean highlightFragments, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws CorruptIndexException, IOException, ParseException {
1569

    
1570

    
1571
        LuceneSearch luceneSearch = prepareFindByFullTextSearch(clazz, queryString, classification, languages, highlightFragments, null);
1572

    
1573
        // --- execute search
1574
        TopGroups<BytesRef> topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
1575

    
1576
        Map<CdmBaseType, String> idFieldMap = new HashMap<CdmBaseType, String>();
1577
        idFieldMap.put(CdmBaseType.TAXON, "id");
1578

    
1579
        // ---  initialize taxa, thighlight matches ....
1580
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1581
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1582
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1583

    
1584
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
1585
        return new DefaultPagerImpl<SearchResult<TaxonBase>>(pageNumber, totalHits, pageSize, searchResults);
1586
    }
1587

    
1588
    @Override
1589
    public Pager<SearchResult<TaxonBase>> findByDistribution(List<NamedArea> areaFilter, List<PresenceAbsenceTerm> statusFilter,
1590
            Classification classification,
1591
            Integer pageSize, Integer pageNumber,
1592
            List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, ParseException {
1593

    
1594
        LuceneSearch luceneSearch = prepareByDistributionSearch(areaFilter, statusFilter, classification);
1595

    
1596
        // --- execute search
1597
        TopGroups<BytesRef> topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
1598

    
1599
        Map<CdmBaseType, String> idFieldMap = new HashMap<CdmBaseType, String>();
1600
        idFieldMap.put(CdmBaseType.TAXON, "id");
1601

    
1602
        // ---  initialize taxa, thighlight matches ....
1603
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1604
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1605
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1606

    
1607
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
1608
        return new DefaultPagerImpl<SearchResult<TaxonBase>>(pageNumber, totalHits, pageSize, searchResults);
1609
    }
1610

    
1611
    /**
1612
     * @param clazz
1613
     * @param queryString
1614
     * @param classification
1615
     * @param languages
1616
     * @param highlightFragments
1617
     * @param sortFields TODO
1618
     * @param directorySelectClass
1619
     * @return
1620
     */
1621
    protected LuceneSearch prepareFindByFullTextSearch(Class<? extends CdmBase> clazz, String queryString, Classification classification, List<Language> languages,
1622
            boolean highlightFragments, SortField[] sortFields) {
1623
        Builder finalQueryBuilder = new Builder();
1624
        Builder textQueryBuilder = new Builder();
1625

    
1626
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1627
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1628

    
1629
        if(sortFields == null){
1630
            sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING,  false)};
1631
        }
1632
        luceneSearch.setSortFields(sortFields);
1633

    
1634
        // ---- search criteria
1635
        luceneSearch.setCdmTypRestriction(clazz);
1636

    
1637
        if(!queryString.isEmpty() && !queryString.equals("*") && !queryString.equals("?") ) {
1638
            textQueryBuilder.add(taxonBaseQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
1639
            textQueryBuilder.add(taxonBaseQueryFactory.newDefinedTermQuery("name.rank", queryString, languages), Occur.SHOULD);
1640
        }
1641

    
1642
        BooleanQuery textQuery = textQueryBuilder.build();
1643
        if(textQuery.clauses().size() > 0) {
1644
            finalQueryBuilder.add(textQuery, Occur.MUST);
1645
        }
1646

    
1647

    
1648
        if(classification != null){
1649
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery("taxonNodes.classification.id", classification), Occur.MUST);
1650
        }
1651
        luceneSearch.setQuery(finalQueryBuilder.build());
1652

    
1653
        if(highlightFragments){
1654
            luceneSearch.setHighlightFields(taxonBaseQueryFactory.getTextFieldNamesAsArray());
1655
        }
1656
        return luceneSearch;
1657
    }
1658

    
1659
    /**
1660
     * Uses org.apache.lucene.search.join.JoinUtil for query time joining, alternatively
1661
     * the BlockJoinQuery could be used. The latter might be more memory save but has the
1662
     * drawback of requiring to do the join an indexing time.
1663
     * see  http://dev.e-taxonomy.eu/trac/wiki/LuceneNotes#JoinsinLucene for more information on this.
1664
     *
1665
     * Joins TaxonRelationShip with Taxon depending on the direction of the given edge:
1666
     * <ul>
1667
     * <li>direct, everted: {@link Direction.relatedTo}: TaxonRelationShip.relatedTo.id --&gt; Taxon.id </li>
1668
     * <li>inverse: {@link Direction.relatedFrom}:  TaxonRelationShip.relatedFrom.id --&gt; Taxon.id </li>
1669
     * <ul>
1670
     * @param queryString
1671
     * @param classification
1672
     * @param languages
1673
     * @param highlightFragments
1674
     * @param sortFields TODO
1675
     *
1676
     * @return
1677
     * @throws IOException
1678
     */
1679
    protected LuceneSearch prepareFindByTaxonRelationFullTextSearch(TaxonRelationshipEdge edge, String queryString, Classification classification, List<Language> languages,
1680
            boolean highlightFragments, SortField[] sortFields) throws IOException {
1681

    
1682
        String fromField;
1683
        String queryTermField;
1684
        String toField = "id"; // TaxonBase.uuid
1685

    
1686
        if(edge.isBidirectional()){
1687
            throw new RuntimeException("Bidirectional joining not supported!");
1688
        }
1689
        if(edge.isEvers()){
1690
            fromField = "relatedFrom.id";
1691
            queryTermField = "relatedFrom.titleCache";
1692
        } else if(edge.isInvers()) {
1693
            fromField = "relatedTo.id";
1694
            queryTermField = "relatedTo.titleCache";
1695
        } else {
1696
            throw new RuntimeException("Invalid direction: " + edge.getDirections());
1697
        }
1698

    
1699
        Builder finalQueryBuilder = new Builder();
1700

    
1701
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1702
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1703

    
1704
        Builder joinFromQueryBuilder = new Builder();
1705
        joinFromQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(queryTermField, queryString), Occur.MUST);
1706
        joinFromQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery("type.id", edge.getTaxonRelationshipType()), Occur.MUST);
1707
        Query joinQuery = taxonBaseQueryFactory.newJoinQuery(TaxonRelationship.class, fromField, false, joinFromQueryBuilder.build(), toField, null, ScoreMode.Max);
1708

    
1709
        if(sortFields == null){
1710
            sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING,  false)};
1711
        }
1712
        luceneSearch.setSortFields(sortFields);
1713

    
1714
        finalQueryBuilder.add(joinQuery, Occur.MUST);
1715

    
1716
        if(classification != null){
1717
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery("taxonNodes.classification.id", classification), Occur.MUST);
1718
        }
1719
        luceneSearch.setQuery(finalQueryBuilder.build());
1720

    
1721
        if(highlightFragments){
1722
            luceneSearch.setHighlightFields(taxonBaseQueryFactory.getTextFieldNamesAsArray());
1723
        }
1724
        return luceneSearch;
1725
    }
1726

    
1727
    @Override
1728
    public Pager<SearchResult<TaxonBase>> findTaxaAndNamesByFullText(
1729
            EnumSet<TaxaAndNamesSearchMode> searchModes, String queryString, Classification classification,
1730
            Set<NamedArea> namedAreas, Set<PresenceAbsenceTerm> distributionStatus, List<Language> languages,
1731
            boolean highlightFragments, Integer pageSize,
1732
            Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths)
1733
            throws CorruptIndexException, IOException, ParseException, LuceneMultiSearchException {
1734

    
1735
        // FIXME: allow taxonomic ordering
1736
        //  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";
1737
        // this require building a special sort column by a special classBridge
1738
        if(highlightFragments){
1739
            logger.warn("findTaxaAndNamesByFullText() : fragment highlighting is " +
1740
                    "currently not fully supported by this method and thus " +
1741
                    "may not work with common names and misapplied names.");
1742
        }
1743

    
1744
        // convert sets to lists
1745
        List<NamedArea> namedAreaList = null;
1746
        List<PresenceAbsenceTerm>distributionStatusList = null;
1747
        if(namedAreas != null){
1748
            namedAreaList = new ArrayList<NamedArea>(namedAreas.size());
1749
            namedAreaList.addAll(namedAreas);
1750
        }
1751
        if(distributionStatus != null){
1752
            distributionStatusList = new ArrayList<PresenceAbsenceTerm>(distributionStatus.size());
1753
            distributionStatusList.addAll(distributionStatus);
1754
        }
1755

    
1756
        // set default if parameter is null
1757
        if(searchModes == null){
1758
            searchModes = EnumSet.of(TaxaAndNamesSearchMode.doTaxa);
1759
        }
1760

    
1761
        // set sort order and thus override any sort orders which may have been
1762
        // defined by prepare*Search methods
1763
        if(orderHints == null){
1764
            orderHints = OrderHint.NOMENCLATURAL_SORT_ORDER.asList();
1765
        }
1766
        SortField[] sortFields = new SortField[orderHints.size()];
1767
        int i = 0;
1768
        for(OrderHint oh : orderHints){
1769
            sortFields[i++] = oh.toSortField();
1770
        }
1771
//        SortField[] sortFields = new SortField[]{SortField.FIELD_SCORE, new SortField("id", SortField.STRING, false)};
1772
//        SortField[] sortFields = new SortField[]{new SortField(NomenclaturalSortOrderBrigde.NAME_SORT_FIELD_NAME, SortField.STRING, false)};
1773

    
1774

    
1775
        boolean addDistributionFilter = namedAreas != null && namedAreas.size() > 0;
1776

    
1777
        List<LuceneSearch> luceneSearches = new ArrayList<LuceneSearch>();
1778
        Map<CdmBaseType, String> idFieldMap = new HashMap<CdmBaseType, String>();
1779

    
1780
        /*
1781
          ======== filtering by distribution , HOWTO ========
1782

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

    
1788

    
1789
          3. how does it work in spatial?
1790
          see
1791
           - http://www.nsshutdown.com/projects/lucene/whitepaper/locallucene_v2.html
1792
           - http://www.infoq.com/articles/LuceneSpatialSupport
1793
           - http://www.mhaller.de/archives/156-Spatial-search-with-Lucene.html
1794
          ------------------------------------------------------------------------
1795

    
1796
          filter strategies:
1797
          A) use a separate distribution filter per index sub-query/search:
1798
           - byTaxonSyonym (query TaxaonBase):
1799
               use a join area filter (Distribution -> TaxonBase)
1800
           - byCommonName (query DescriptionElementBase): use an area filter on
1801
               DescriptionElementBase !!! PROBLEM !!!
1802
               This cannot work since the distributions are different entities than the
1803
               common names and thus these are different lucene documents.
1804
           - byMisaplliedNames (join query TaxonRelationship -> TaxaonBase):
1805
               use a join area filter (Distribution -> TaxonBase)
1806

    
1807
          B) use a common distribution filter for all index sub-query/searches:
1808
           - use a common join area filter (Distribution -> TaxonBase)
1809
           - also implement the byCommonName as join query (CommonName -> TaxonBase)
1810
           PROBLEM in this case: we are losing the fragment highlighting for the
1811
           common names, since the returned documents are always TaxonBases
1812
        */
1813

    
1814
        /* The QueryFactory for creating filter queries on Distributions should
1815
         * The query factory used for the common names query cannot be reused
1816
         * for this case, since we want to only record the text fields which are
1817
         * actually used in the primary query
1818
         */
1819
        QueryFactory distributionFilterQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Distribution.class);
1820

    
1821
        Builder multiIndexByAreaFilterBuilder = new Builder();
1822

    
1823
        // search for taxa or synonyms
1824
        if(searchModes.contains(TaxaAndNamesSearchMode.doTaxa) || searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)) {
1825
            Class taxonBaseSubclass = TaxonBase.class;
1826
            if(searchModes.contains(TaxaAndNamesSearchMode.doTaxa) && !searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
1827
                taxonBaseSubclass = Taxon.class;
1828
            } else if (!searchModes.contains(TaxaAndNamesSearchMode.doTaxa) && searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)) {
1829
                taxonBaseSubclass = Synonym.class;
1830
            }
1831
            luceneSearches.add(prepareFindByFullTextSearch(taxonBaseSubclass, queryString, classification, languages, highlightFragments, sortFields));
1832
            idFieldMap.put(CdmBaseType.TAXON, "id");
1833
            /* A) does not work!!!!
1834
            if(addDistributionFilter){
1835
                // in this case we need a filter which uses a join query
1836
                // to get the TaxonBase documents for the DescriptionElementBase documents
1837
                // which are matching the areas in question
1838
                Query taxonAreaJoinQuery = createByDistributionJoinQuery(
1839
                        namedAreaList,
1840
                        distributionStatusList,
1841
                        distributionFilterQueryFactory
1842
                        );
1843
                multiIndexByAreaFilter.add(new QueryWrapperFilter(taxonAreaJoinQuery), Occur.SHOULD);
1844
            }
1845
            */
1846
            if(addDistributionFilter && searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
1847
                // add additional area filter for synonyms
1848
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1849
                String toField = "accTaxon.id"; // id in TaxonBase index (is multivalued)
1850

    
1851
                //TODO replace by createByDistributionJoinQuery
1852
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
1853
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class, fromField, true, byDistributionQuery, toField, Taxon.class, ScoreMode.None);
1854
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1855

    
1856
            }
1857
        }
1858

    
1859
        // search by CommonTaxonName
1860
        if(searchModes.contains(TaxaAndNamesSearchMode.doTaxaByCommonNames)) {
1861
            // B)
1862
            QueryFactory descriptionElementQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(DescriptionElementBase.class);
1863
            Query byCommonNameJoinQuery = descriptionElementQueryFactory.newJoinQuery(
1864
                    CommonTaxonName.class,
1865
                    "inDescription.taxon.id",
1866
                    true,
1867
                    QueryFactory.addTypeRestriction(
1868
                                createByDescriptionElementFullTextQuery(queryString, classification, null, languages, descriptionElementQueryFactory)
1869
                                , CommonTaxonName.class
1870
                                ).build(), "id", null, ScoreMode.Max);
1871
            logger.debug("byCommonNameJoinQuery: " + byCommonNameJoinQuery.toString());
1872
            LuceneSearch byCommonNameSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
1873
            byCommonNameSearch.setCdmTypRestriction(Taxon.class);
1874
            byCommonNameSearch.setQuery(byCommonNameJoinQuery);
1875
            byCommonNameSearch.setSortFields(sortFields);
1876
            idFieldMap.put(CdmBaseType.TAXON, "id");
1877

    
1878
            luceneSearches.add(byCommonNameSearch);
1879

    
1880
            /* A) does not work!!!!
1881
            luceneSearches.add(
1882
                    prepareByDescriptionElementFullTextSearch(CommonTaxonName.class,
1883
                            queryString, classification, null, languages, highlightFragments)
1884
                        );
1885
            idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
1886
            if(addDistributionFilter){
1887
                // in this case we are able to use DescriptionElementBase documents
1888
                // which are matching the areas in question directly
1889
                BooleanQuery byDistributionQuery = createByDistributionQuery(
1890
                        namedAreaList,
1891
                        distributionStatusList,
1892
                        distributionFilterQueryFactory
1893
                        );
1894
                multiIndexByAreaFilter.add(new QueryWrapperFilter(byDistributionQuery), Occur.SHOULD);
1895
            } */
1896
        }
1897

    
1898
        // search by misapplied names
1899
        if(searchModes.contains(TaxaAndNamesSearchMode.doMisappliedNames)) {
1900
            // NOTE:
1901
            // prepareFindByTaxonRelationFullTextSearch() is making use of JoinUtil.createJoinQuery()
1902
            // which allows doing query time joins
1903
            // finds the misapplied name (Taxon B) which is an misapplication for
1904
            // a related Taxon A.
1905
            //
1906
            luceneSearches.add(prepareFindByTaxonRelationFullTextSearch(
1907
                    new TaxonRelationshipEdge(TaxonRelationshipType.MISAPPLIED_NAME_FOR(), Direction.relatedTo),
1908
                    queryString, classification, languages, highlightFragments, sortFields));
1909
            idFieldMap.put(CdmBaseType.TAXON, "id");
1910

    
1911
            if(addDistributionFilter){
1912
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1913

    
1914
                /*
1915
                 * Here i was facing wired and nasty bug which took me bugging be really for hours until I found this solution.
1916
                 * Maybe this is a bug in java itself java.
1917
                 *
1918
                 * When the string toField is constructed by using the expression TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString()
1919
                 * directly:
1920
                 *
1921
                 *    String toField = "relation." + TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString() +".to.id";
1922
                 *
1923
                 * The byDistributionQuery fails, however when the uuid is first stored in another string variable the query
1924
                 * will execute as expected:
1925
                 *
1926
                 *    String misappliedNameForUuid = TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString();
1927
                 *    String toField = "relation." + misappliedNameForUuid +".to.id";
1928
                 *
1929
                 * Comparing both strings by the String.equals method returns true, so both String are identical.
1930
                 *
1931
                 * The bug occurs when running eu.etaxonomy.cdm.api.service.TaxonServiceSearchTest in eclipse and in maven and seems to to be
1932
                 * 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)
1933
                 * The bug is persistent after a reboot of the development computer.
1934
                 */
1935
//                String misappliedNameForUuid = TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString();
1936
//                String toField = "relation." + misappliedNameForUuid +".to.id";
1937
                String toField = "relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id";
1938
//                System.out.println("relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id".equals("relation." + misappliedNameForUuid +".to.id") ? " > identical" : " > different");
1939
//                System.out.println("relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id".equals("relation." + TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString() +".to.id") ? " > identical" : " > different");
1940

    
1941
                //TODO replace by createByDistributionJoinQuery
1942
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
1943
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class, fromField, true, byDistributionQuery, toField, null, ScoreMode.None);
1944

    
1945
//                debug code for bug described above
1946
                //does not compile anymore since changing from lucene 3.6.2 to lucene 4.10+
1947
//                DocIdSet filterMatchSet = filter.getDocIdSet(luceneIndexToolProvider.getIndexReaderFor(Taxon.class));
1948
//                System.err.println(DocIdBitSetPrinter.docsAsString(filterMatchSet, 100));
1949

    
1950
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1951
            }
1952
        }
1953

    
1954
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider,
1955
                luceneSearches.toArray(new LuceneSearch[luceneSearches.size()]));
1956

    
1957

    
1958
        if(addDistributionFilter){
1959

    
1960
            // B)
1961
            // in this case we need a filter which uses a join query
1962
            // to get the TaxonBase documents for the DescriptionElementBase documents
1963
            // which are matching the areas in question
1964
            //
1965
            // for toTaxa, doByCommonName
1966
            Query taxonAreaJoinQuery = createByDistributionJoinQuery(
1967
                    namedAreaList,
1968
                    distributionStatusList,
1969
                    distributionFilterQueryFactory,
1970
                    Taxon.class, true
1971
                    );
1972
            multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1973
        }
1974

    
1975
        if (addDistributionFilter){
1976
            multiSearch.setFilter(multiIndexByAreaFilterBuilder.build());
1977
        }
1978

    
1979

    
1980
        // --- execute search
1981
        TopGroups<BytesRef> topDocsResultSet = multiSearch.executeSearch(pageSize, pageNumber);
1982

    
1983
        // --- initialize taxa, highlight matches ....
1984
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
1985

    
1986

    
1987
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1988
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1989

    
1990
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
1991
        return new DefaultPagerImpl<SearchResult<TaxonBase>>(pageNumber, totalHits, pageSize, searchResults);
1992
    }
1993

    
1994
    /**
1995
     * @param namedAreaList at least one area must be in the list
1996
     * @param distributionStatusList optional
1997
     * @param toType toType
1998
     *      Optional parameter. Only used for debugging to print the toType documents
1999
     * @param asFilter TODO
2000
     * @return
2001
     * @throws IOException
2002
     */
2003
    protected Query createByDistributionJoinQuery(
2004
            List<NamedArea> namedAreaList,
2005
            List<PresenceAbsenceTerm> distributionStatusList,
2006
            QueryFactory queryFactory, Class<? extends CdmBase> toType, boolean asFilter
2007
            ) throws IOException {
2008

    
2009
        String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
2010
        String toField = "id"; // id in toType usually this is the TaxonBase index
2011

    
2012
        BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, queryFactory);
2013

    
2014
        ScoreMode scoreMode = asFilter ?  ScoreMode.None : ScoreMode.Max;
2015

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

    
2018
        return taxonAreaJoinQuery;
2019
    }
2020

    
2021
    /**
2022
     * @param namedAreaList
2023
     * @param distributionStatusList
2024
     * @param queryFactory
2025
     * @return
2026
     */
2027
    private BooleanQuery createByDistributionQuery(List<NamedArea> namedAreaList,
2028
            List<PresenceAbsenceTerm> distributionStatusList, QueryFactory queryFactory) {
2029
        Builder areaQueryBuilder = new Builder();
2030
        // area field from Distribution
2031
        areaQueryBuilder.add(queryFactory.newEntityIdsQuery("area.id", namedAreaList), Occur.MUST);
2032

    
2033
        // status field from Distribution
2034
        if(distributionStatusList != null && distributionStatusList.size() > 0){
2035
            areaQueryBuilder.add(queryFactory.newEntityIdsQuery("status.id", distributionStatusList), Occur.MUST);
2036
        }
2037

    
2038
        BooleanQuery areaQuery = areaQueryBuilder.build();
2039
        logger.debug("createByDistributionQuery() query: " + areaQuery.toString());
2040
        return areaQuery;
2041
    }
2042

    
2043
    /**
2044
     * This method has been primarily created for testing the area join query but might
2045
     * also be useful in other situations
2046
     *
2047
     * @param namedAreaList
2048
     * @param distributionStatusList
2049
     * @param classification
2050
     * @param highlightFragments
2051
     * @return
2052
     * @throws IOException
2053
     */
2054
    protected LuceneSearch prepareByDistributionSearch(
2055
            List<NamedArea> namedAreaList, List<PresenceAbsenceTerm> distributionStatusList,
2056
            Classification classification) throws IOException {
2057

    
2058
        Builder finalQueryBuilder = new Builder();
2059

    
2060
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
2061

    
2062
        // FIXME is this query factory using the wrong type?
2063
        QueryFactory taxonQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Taxon.class);
2064

    
2065
        SortField[] sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING, false)};
2066
        luceneSearch.setSortFields(sortFields);
2067

    
2068

    
2069
        Query byAreaQuery = createByDistributionJoinQuery(namedAreaList, distributionStatusList, taxonQueryFactory, null, false);
2070

    
2071
        finalQueryBuilder.add(byAreaQuery, Occur.MUST);
2072

    
2073
        if(classification != null){
2074
            finalQueryBuilder.add(taxonQueryFactory.newEntityIdQuery("taxonNodes.classification.id", classification), Occur.MUST);
2075
        }
2076
        BooleanQuery finalQuery = finalQueryBuilder.build();
2077
        logger.info("prepareByAreaSearch() query: " + finalQuery.toString());
2078
        luceneSearch.setQuery(finalQuery);
2079

    
2080
        return luceneSearch;
2081
    }
2082

    
2083
    @Override
2084
    public Pager<SearchResult<TaxonBase>> findByDescriptionElementFullText(
2085
            Class<? extends DescriptionElementBase> clazz, String queryString,
2086
            Classification classification, List<Feature> features, List<Language> languages,
2087
            boolean highlightFragments, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws CorruptIndexException, IOException, ParseException {
2088

    
2089

    
2090
        LuceneSearch luceneSearch = prepareByDescriptionElementFullTextSearch(clazz, queryString, classification, features, languages, highlightFragments);
2091

    
2092
        // --- execute search
2093
        TopGroups<BytesRef> topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
2094

    
2095
        Map<CdmBaseType, String> idFieldMap = new HashMap<CdmBaseType, String>();
2096
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2097

    
2098
        // --- initialize taxa, highlight matches ....
2099
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
2100
        @SuppressWarnings("rawtypes")
2101
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2102
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2103

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

    
2107
    }
2108

    
2109

    
2110
    @Override
2111
    public Pager<SearchResult<TaxonBase>> findByEverythingFullText(String queryString,
2112
            Classification classification, List<Language> languages, boolean highlightFragments,
2113
            Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws CorruptIndexException, IOException, ParseException, LuceneMultiSearchException {
2114

    
2115
        LuceneSearch luceneSearchByDescriptionElement = prepareByDescriptionElementFullTextSearch(null, queryString, classification, null, languages, highlightFragments);
2116
        LuceneSearch luceneSearchByTaxonBase = prepareFindByFullTextSearch(null, queryString, classification, languages, highlightFragments, null);
2117

    
2118
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider, luceneSearchByDescriptionElement, luceneSearchByTaxonBase);
2119

    
2120
        // --- execute search
2121
        TopGroups<BytesRef> topDocsResultSet = multiSearch.executeSearch(pageSize, pageNumber);
2122

    
2123
        // --- initialize taxa, highlight matches ....
2124
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
2125

    
2126
        Map<CdmBaseType, String> idFieldMap = new HashMap<CdmBaseType, String>();
2127
        idFieldMap.put(CdmBaseType.TAXON, "id");
2128
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2129

    
2130
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2131
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2132

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

    
2136
    }
2137

    
2138

    
2139
    /**
2140
     * @param clazz
2141
     * @param queryString
2142
     * @param classification
2143
     * @param features
2144
     * @param languages
2145
     * @param highlightFragments
2146
     * @param directorySelectClass
2147
     * @return
2148
     */
2149
    protected LuceneSearch prepareByDescriptionElementFullTextSearch(Class<? extends CdmBase> clazz,
2150
            String queryString, Classification classification, List<Feature> features,
2151
            List<Language> languages, boolean highlightFragments) {
2152

    
2153
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, DescriptionElementBase.class);
2154
        QueryFactory descriptionElementQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(DescriptionElementBase.class);
2155

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

    
2158
        BooleanQuery finalQuery = createByDescriptionElementFullTextQuery(queryString, classification, features,
2159
                languages, descriptionElementQueryFactory);
2160

    
2161
        luceneSearch.setSortFields(sortFields);
2162
        luceneSearch.setCdmTypRestriction(clazz);
2163
        luceneSearch.setQuery(finalQuery);
2164
        if(highlightFragments){
2165
            luceneSearch.setHighlightFields(descriptionElementQueryFactory.getTextFieldNamesAsArray());
2166
        }
2167

    
2168
        return luceneSearch;
2169
    }
2170

    
2171
    /**
2172
     * @param queryString
2173
     * @param classification
2174
     * @param features
2175
     * @param languages
2176
     * @param descriptionElementQueryFactory
2177
     * @return
2178
     */
2179
    private BooleanQuery createByDescriptionElementFullTextQuery(String queryString, Classification classification,
2180
            List<Feature> features, List<Language> languages, QueryFactory descriptionElementQueryFactory) {
2181
        Builder finalQueryBuilder = new Builder();
2182
        Builder textQueryBuilder = new Builder();
2183
        textQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
2184

    
2185
        // common name
2186
        Builder nameQueryBuilder = new Builder();
2187
        if(languages == null || languages.size() == 0){
2188
            nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2189
        } else {
2190
            Builder languageSubQueryBuilder = new Builder();
2191
            for(Language lang : languages){
2192
                languageSubQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("language.uuid",  lang.getUuid().toString(), false), Occur.SHOULD);
2193
            }
2194
            nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2195
            nameQueryBuilder.add(languageSubQueryBuilder.build(), Occur.MUST);
2196
        }
2197
        textQueryBuilder.add(nameQueryBuilder.build(), Occur.SHOULD);
2198

    
2199

    
2200
        // text field from TextData
2201
        textQueryBuilder.add(descriptionElementQueryFactory.newMultilanguageTextQuery("text", queryString, languages), Occur.SHOULD);
2202

    
2203
        // --- TermBase fields - by representation ----
2204
        // state field from CategoricalData
2205
        textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.state", queryString, languages), Occur.SHOULD);
2206

    
2207
        // state field from CategoricalData
2208
        textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.modifyingText", queryString, languages), Occur.SHOULD);
2209

    
2210
        // area field from Distribution
2211
        textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("area", queryString, languages), Occur.SHOULD);
2212

    
2213
        // status field from Distribution
2214
        textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("status", queryString, languages), Occur.SHOULD);
2215

    
2216
        finalQueryBuilder.add(textQueryBuilder.build(), Occur.MUST);
2217
        // --- classification ----
2218

    
2219
        if(classification != null){
2220
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityIdQuery("inDescription.taxon.taxonNodes.classification.id", classification), Occur.MUST);
2221
        }
2222

    
2223
        // --- IdentifieableEntity fields - by uuid
2224
        if(features != null && features.size() > 0 ){
2225
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityUuidsQuery("feature.uuid", features), Occur.MUST);
2226
        }
2227

    
2228
        // the description must be associated with a taxon
2229
        finalQueryBuilder.add(descriptionElementQueryFactory.newIsNotNullQuery("inDescription.taxon.id"), Occur.MUST);
2230

    
2231
        BooleanQuery finalQuery = finalQueryBuilder.build();
2232
        logger.info("prepareByDescriptionElementFullTextSearch() query: " + finalQuery.toString());
2233
        return finalQuery;
2234
    }
2235

    
2236
    /**
2237
     * DefinedTerm representations and MultilanguageString maps are stored in the Lucene index by the {@link DefinedTermBaseClassBridge}
2238
     * and {@link MultilanguageTextFieldBridge } in a consistent way. One field per language and also in one additional field for all languages.
2239
     * This method is a convenient means to retrieve a Lucene query string for such the fields.
2240
     *
2241
     * @param name name of the term field as in the Lucene index. Must be field created by {@link DefinedTermBaseClassBridge}
2242
     * or {@link MultilanguageTextFieldBridge }
2243
     * @param languages the languages to search for exclusively. Can be <code>null</code> to search in all languages
2244
     * @param stringBuilder a StringBuilder to be reused, if <code>null</code> a new StringBuilder will be instantiated and is returned
2245
     * @return the StringBuilder given a parameter or a new one if the stringBuilder parameter was null.
2246
     *
2247
     * TODO move to utiliy class !!!!!!!!
2248
     */
2249
    private StringBuilder appendLocalizedFieldQuery(String name, List<Language> languages, StringBuilder stringBuilder) {
2250

    
2251
        if(stringBuilder == null){
2252
            stringBuilder = new StringBuilder();
2253
        }
2254
        if(languages == null || languages.size() == 0){
2255
            stringBuilder.append(name + ".ALL:(%1$s) ");
2256
        } else {
2257
            for(Language lang : languages){
2258
                stringBuilder.append(name + "." + lang.getUuid().toString() + ":(%1$s) ");
2259
            }
2260
        }
2261
        return stringBuilder;
2262
    }
2263

    
2264
    @Override
2265
    public List<Synonym> createInferredSynonyms(Taxon taxon, Classification classification, SynonymRelationshipType type, boolean doWithMisappliedNames){
2266
        List <Synonym> inferredSynonyms = new ArrayList<Synonym>();
2267
        List<Synonym> inferredSynonymsToBeRemoved = new ArrayList<Synonym>();
2268

    
2269
        HashMap <UUID, ZoologicalName> zooHashMap = new HashMap<UUID, ZoologicalName>();
2270

    
2271

    
2272
        UUID nameUuid= taxon.getName().getUuid();
2273
        ZoologicalName taxonName = getZoologicalName(nameUuid, zooHashMap);
2274
        String epithetOfTaxon = null;
2275
        String infragenericEpithetOfTaxon = null;
2276
        String infraspecificEpithetOfTaxon = null;
2277
        if (taxonName.isSpecies()){
2278
             epithetOfTaxon= taxonName.getSpecificEpithet();
2279
        } else if (taxonName.isInfraGeneric()){
2280
            infragenericEpithetOfTaxon = taxonName.getInfraGenericEpithet();
2281
        } else if (taxonName.isInfraSpecific()){
2282
            infraspecificEpithetOfTaxon = taxonName.getInfraSpecificEpithet();
2283
        }
2284
        String genusOfTaxon = taxonName.getGenusOrUninomial();
2285
        Set<TaxonNode> nodes = taxon.getTaxonNodes();
2286
        List<String> taxonNames = new ArrayList<String>();
2287

    
2288
        for (TaxonNode node: nodes){
2289
           // HashMap<String, String> synonymsGenus = new HashMap<String, String>(); // Changed this to be able to store the idInSource to a genusName
2290
           // List<String> synonymsEpithet = new ArrayList<String>();
2291

    
2292
            if (node.getClassification().equals(classification)){
2293
                if (!node.isTopmostNode()){
2294
                    TaxonNode parent = node.getParent();
2295
                    parent = HibernateProxyHelper.deproxy(parent);
2296
                    TaxonNameBase<?,?> parentName =  parent.getTaxon().getName();
2297
                    ZoologicalName zooParentName = HibernateProxyHelper.deproxy(parentName, ZoologicalName.class);
2298
                    Taxon parentTaxon = HibernateProxyHelper.deproxy(parent.getTaxon());
2299
                    Rank rankOfTaxon = taxonName.getRank();
2300

    
2301

    
2302
                    //create inferred synonyms for species, subspecies
2303
                    if ((parentName.isGenus() || parentName.isSpecies() || parentName.getRank().equals(Rank.SUBGENUS())) ){
2304

    
2305
                        Synonym inferredEpithet = null;
2306
                        Synonym inferredGenus = null;
2307
                        Synonym potentialCombination = null;
2308

    
2309
                        List<String> propertyPaths = new ArrayList<String>();
2310
                        propertyPaths.add("synonym");
2311
                        propertyPaths.add("synonym.name");
2312
                        List<OrderHint> orderHints = new ArrayList<OrderHint>();
2313
                        orderHints.add(new OrderHint("relatedFrom.titleCache", SortOrder.ASCENDING));
2314

    
2315
                        List<SynonymRelationship> synonymRelationshipsOfParent = dao.getSynonyms(parentTaxon, SynonymRelationshipType.HETEROTYPIC_SYNONYM_OF(), null, null,orderHints,propertyPaths);
2316
                        List<SynonymRelationship> synonymRelationshipsOfTaxon= dao.getSynonyms(taxon, SynonymRelationshipType.HETEROTYPIC_SYNONYM_OF(), null, null,orderHints,propertyPaths);
2317

    
2318
                        List<TaxonRelationship> taxonRelListParent = null;
2319
                        List<TaxonRelationship> taxonRelListTaxon = null;
2320
                        if (doWithMisappliedNames){
2321
                            taxonRelListParent = dao.getTaxonRelationships(parentTaxon, TaxonRelationshipType.MISAPPLIED_NAME_FOR(), null, null, orderHints, propertyPaths, Direction.relatedTo);
2322
                            taxonRelListTaxon = dao.getTaxonRelationships(taxon, TaxonRelationshipType.MISAPPLIED_NAME_FOR(), null, null, orderHints, propertyPaths, Direction.relatedTo);
2323
                        }
2324

    
2325

    
2326
                        if (type.equals(SynonymRelationshipType.INFERRED_EPITHET_OF())){
2327

    
2328

    
2329
                            for (SynonymRelationship synonymRelationOfParent:synonymRelationshipsOfParent){
2330
                                Synonym syn = synonymRelationOfParent.getSynonym();
2331

    
2332
                                inferredEpithet = createInferredEpithets(taxon,
2333
                                        zooHashMap, taxonName, epithetOfTaxon,
2334
                                        infragenericEpithetOfTaxon,
2335
                                        infraspecificEpithetOfTaxon,
2336
                                        taxonNames, parentName,
2337
                                        syn);
2338

    
2339

    
2340
                                inferredSynonyms.add(inferredEpithet);
2341
                                zooHashMap.put(inferredEpithet.getName().getUuid(), (ZoologicalName)inferredEpithet.getName());
2342
                                taxonNames.add(((ZoologicalName)inferredEpithet.getName()).getNameCache());
2343
                            }
2344

    
2345
                            if (doWithMisappliedNames){
2346

    
2347
                                for (TaxonRelationship taxonRelationship: taxonRelListParent){
2348
                                     Taxon misappliedName = taxonRelationship.getFromTaxon();
2349

    
2350
                                     inferredEpithet = createInferredEpithets(taxon,
2351
                                             zooHashMap, taxonName, epithetOfTaxon,
2352
                                             infragenericEpithetOfTaxon,
2353
                                             infraspecificEpithetOfTaxon,
2354
                                             taxonNames, parentName,
2355
                                             misappliedName);
2356

    
2357
                                    inferredSynonyms.add(inferredEpithet);
2358
                                    zooHashMap.put(inferredEpithet.getName().getUuid(), (ZoologicalName)inferredEpithet.getName());
2359
                                     taxonNames.add(((ZoologicalName)inferredEpithet.getName()).getNameCache());
2360
                                }
2361
                            }
2362

    
2363
                            if (!taxonNames.isEmpty()){
2364
                            List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2365
                            ZoologicalName name;
2366
                            if (!synNotInCDM.isEmpty()){
2367
                                inferredSynonymsToBeRemoved.clear();
2368

    
2369
                                for (Synonym syn :inferredSynonyms){
2370
                                    name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2371
                                    if (!synNotInCDM.contains(name.getNameCache())){
2372
                                        inferredSynonymsToBeRemoved.add(syn);
2373
                                    }
2374
                                }
2375

    
2376
                                // Remove identified Synonyms from inferredSynonyms
2377
                                for (Synonym synonym : inferredSynonymsToBeRemoved) {
2378
                                    inferredSynonyms.remove(synonym);
2379
                                }
2380
                            }
2381
                        }
2382

    
2383
                    }else if (type.equals(SynonymRelationshipType.INFERRED_GENUS_OF())){
2384

    
2385

    
2386
                        for (SynonymRelationship synonymRelationOfTaxon:synonymRelationshipsOfTaxon){
2387
                            TaxonNameBase synName;
2388
                            ZoologicalName inferredSynName;
2389

    
2390
                            Synonym syn = synonymRelationOfTaxon.getSynonym();
2391
                            inferredGenus = createInferredGenus(taxon,
2392
                                    zooHashMap, taxonName, epithetOfTaxon,
2393
                                    genusOfTaxon, taxonNames, zooParentName, syn);
2394

    
2395
                            inferredSynonyms.add(inferredGenus);
2396
                            zooHashMap.put(inferredGenus.getName().getUuid(), (ZoologicalName)inferredGenus.getName());
2397
                            taxonNames.add(( (ZoologicalName)inferredGenus.getName()).getNameCache());
2398

    
2399

    
2400
                        }
2401

    
2402
                        if (doWithMisappliedNames){
2403

    
2404
                            for (TaxonRelationship taxonRelationship: taxonRelListTaxon){
2405
                                Taxon misappliedName = taxonRelationship.getFromTaxon();
2406
                                inferredGenus = createInferredGenus(taxon, zooHashMap, taxonName, infraspecificEpithetOfTaxon, genusOfTaxon, taxonNames, zooParentName,  misappliedName);
2407

    
2408
                                inferredSynonyms.add(inferredGenus);
2409
                                zooHashMap.put(inferredGenus.getName().getUuid(), (ZoologicalName)inferredGenus.getName());
2410
                                 taxonNames.add(( (ZoologicalName)inferredGenus.getName()).getNameCache());
2411
                            }
2412
                        }
2413

    
2414

    
2415
                        if (!taxonNames.isEmpty()){
2416
                            List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2417
                            ZoologicalName name;
2418
                            if (!synNotInCDM.isEmpty()){
2419
                                inferredSynonymsToBeRemoved.clear();
2420

    
2421
                                for (Synonym syn :inferredSynonyms){
2422
                                    name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2423
                                    if (!synNotInCDM.contains(name.getNameCache())){
2424
                                        inferredSynonymsToBeRemoved.add(syn);
2425
                                    }
2426
                                }
2427

    
2428
                                // Remove identified Synonyms from inferredSynonyms
2429
                                for (Synonym synonym : inferredSynonymsToBeRemoved) {
2430
                                    inferredSynonyms.remove(synonym);
2431
                                }
2432
                            }
2433
                        }
2434

    
2435
                    }else if (type.equals(SynonymRelationshipType.POTENTIAL_COMBINATION_OF())){
2436

    
2437
                        Reference sourceReference = null; // TODO: Determination of sourceReference is redundant
2438
                        ZoologicalName inferredSynName;
2439
                        //for all synonyms of the parent...
2440
                        for (SynonymRelationship synonymRelationOfParent:synonymRelationshipsOfParent){
2441
                            TaxonNameBase synName;
2442
                            Synonym synParent = synonymRelationOfParent.getSynonym();
2443
                            synName = synParent.getName();
2444

    
2445
                            HibernateProxyHelper.deproxy(synParent);
2446

    
2447
                            // Set the sourceReference
2448
                            sourceReference = synParent.getSec();
2449

    
2450
                            // Determine the idInSource
2451
                            String idInSourceParent = getIdInSource(synParent);
2452

    
2453
                            ZoologicalName parentSynZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2454
                            String synParentGenus = parentSynZooName.getGenusOrUninomial();
2455
                            String synParentInfragenericName = null;
2456
                            String synParentSpecificEpithet = null;
2457

    
2458
                            if (parentSynZooName.isInfraGeneric()){
2459
                                synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2460
                            }
2461
                            if (parentSynZooName.isSpecies()){
2462
                                synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2463
                            }
2464

    
2465
                           /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2466
                                synonymsGenus.put(synGenusName, idInSource);
2467
                            }*/
2468

    
2469
                            //for all synonyms of the taxon
2470

    
2471
                            for (SynonymRelationship synonymRelationOfTaxon:synonymRelationshipsOfTaxon){
2472

    
2473
                                Synonym syn = synonymRelationOfTaxon.getSynonym();
2474
                                ZoologicalName zooSynName = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2475
                                potentialCombination = createPotentialCombination(idInSourceParent, parentSynZooName, zooSynName,
2476
                                        synParentGenus,
2477
                                        synParentInfragenericName,
2478
                                        synParentSpecificEpithet, syn, zooHashMap);
2479

    
2480
                                taxon.addSynonym(potentialCombination, SynonymRelationshipType.POTENTIAL_COMBINATION_OF());
2481
                                inferredSynonyms.add(potentialCombination);
2482
                                zooHashMap.put(potentialCombination.getName().getUuid(), (ZoologicalName)potentialCombination.getName());
2483
                                 taxonNames.add(( (ZoologicalName)potentialCombination.getName()).getNameCache());
2484

    
2485
                            }
2486

    
2487

    
2488
                        }
2489

    
2490
                        if (doWithMisappliedNames){
2491

    
2492
                            for (TaxonRelationship parentRelationship: taxonRelListParent){
2493

    
2494
                                TaxonNameBase misappliedParentName;
2495

    
2496
                                Taxon misappliedParent = parentRelationship.getFromTaxon();
2497
                                misappliedParentName = misappliedParent.getName();
2498

    
2499
                                HibernateProxyHelper.deproxy(misappliedParent);
2500

    
2501
                                // Set the sourceReference
2502
                                sourceReference = misappliedParent.getSec();
2503

    
2504
                                // Determine the idInSource
2505
                                String idInSourceParent = getIdInSource(misappliedParent);
2506

    
2507
                                ZoologicalName parentSynZooName = getZoologicalName(misappliedParentName.getUuid(), zooHashMap);
2508
                                String synParentGenus = parentSynZooName.getGenusOrUninomial();
2509
                                String synParentInfragenericName = null;
2510
                                String synParentSpecificEpithet = null;
2511

    
2512
                                if (parentSynZooName.isInfraGeneric()){
2513
                                    synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2514
                                }
2515
                                if (parentSynZooName.isSpecies()){
2516
                                    synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2517
                                }
2518

    
2519

    
2520
                                for (TaxonRelationship taxonRelationship: taxonRelListTaxon){
2521
                                    Taxon misappliedName = taxonRelationship.getFromTaxon();
2522
                                    ZoologicalName zooMisappliedName = getZoologicalName(misappliedName.getName().getUuid(), zooHashMap);
2523
                                    potentialCombination = createPotentialCombination(
2524
                                            idInSourceParent, parentSynZooName, zooMisappliedName,
2525
                                            synParentGenus,
2526
                                            synParentInfragenericName,
2527
                                            synParentSpecificEpithet, misappliedName, zooHashMap);
2528

    
2529

    
2530
                                    taxon.addSynonym(potentialCombination, SynonymRelationshipType.POTENTIAL_COMBINATION_OF());
2531
                                    inferredSynonyms.add(potentialCombination);
2532
                                    zooHashMap.put(potentialCombination.getName().getUuid(), (ZoologicalName)potentialCombination.getName());
2533
                                     taxonNames.add(( (ZoologicalName)potentialCombination.getName()).getNameCache());
2534
                                }
2535
                            }
2536
                        }
2537

    
2538
                        if (!taxonNames.isEmpty()){
2539
                            List<String> synNotInCDM = dao.taxaByNameNotInDB(taxonNames);
2540
                            ZoologicalName name;
2541
                            if (!synNotInCDM.isEmpty()){
2542
                                inferredSynonymsToBeRemoved.clear();
2543
                                for (Synonym syn :inferredSynonyms){
2544
                                    try{
2545
                                        name = (ZoologicalName) syn.getName();
2546
                                    }catch (ClassCastException e){
2547
                                        name = getZoologicalName(syn.getName().getUuid(), zooHashMap);
2548
                                    }
2549
                                    if (!synNotInCDM.contains(name.getNameCache())){
2550
                                        inferredSynonymsToBeRemoved.add(syn);
2551
                                    }
2552
                                 }
2553
                                // Remove identified Synonyms from inferredSynonyms
2554
                                for (Synonym synonym : inferredSynonymsToBeRemoved) {
2555
                                    inferredSynonyms.remove(synonym);
2556
                                }
2557
                            }
2558
                         }
2559
                        }
2560
                    }else {
2561
                        logger.info("The synonymrelationship type is not defined.");
2562
                        return inferredSynonyms;
2563
                    }
2564
                }
2565
            }
2566

    
2567
        }
2568

    
2569
        return inferredSynonyms;
2570
    }
2571

    
2572
    private Synonym createPotentialCombination(String idInSourceParent,
2573
            ZoologicalName parentSynZooName, 	ZoologicalName zooSynName, String synParentGenus,
2574
            String synParentInfragenericName, String synParentSpecificEpithet,
2575
            TaxonBase syn, HashMap<UUID, ZoologicalName> zooHashMap) {
2576
        Synonym potentialCombination;
2577
        Reference sourceReference;
2578
        ZoologicalName inferredSynName;
2579
        HibernateProxyHelper.deproxy(syn);
2580

    
2581
        // Set sourceReference
2582
        sourceReference = syn.getSec();
2583
        if (sourceReference == null){
2584
            logger.warn("The synonym has no sec reference because it is a misapplied name! Take the sec reference of taxon");
2585
            //TODO:Remove
2586
            if (!parentSynZooName.getTaxa().isEmpty()){
2587
                TaxonBase taxon = parentSynZooName.getTaxa().iterator().next();
2588

    
2589
                sourceReference = taxon.getSec();
2590
            }
2591
        }
2592
        String synTaxonSpecificEpithet = zooSynName.getSpecificEpithet();
2593

    
2594
        String synTaxonInfraSpecificName= null;
2595

    
2596
        if (parentSynZooName.isSpecies()){
2597
            synTaxonInfraSpecificName = zooSynName.getInfraSpecificEpithet();
2598
        }
2599

    
2600
        /*if (epithetName != null && !synonymsEpithet.contains(epithetName)){
2601
            synonymsEpithet.add(epithetName);
2602
        }*/
2603

    
2604
        //create potential combinations...
2605
        inferredSynName = ZoologicalName.NewInstance(syn.getName().getRank());
2606

    
2607
        inferredSynName.setGenusOrUninomial(synParentGenus);
2608
        if (zooSynName.isSpecies()){
2609
              inferredSynName.setSpecificEpithet(synTaxonSpecificEpithet);
2610
              if (parentSynZooName.isInfraGeneric()){
2611
                  inferredSynName.setInfraGenericEpithet(synParentInfragenericName);
2612
              }
2613
        }
2614
        if (zooSynName.isInfraSpecific()){
2615
            inferredSynName.setSpecificEpithet(synParentSpecificEpithet);
2616
            inferredSynName.setInfraSpecificEpithet(synTaxonInfraSpecificName);
2617
        }
2618
        if (parentSynZooName.isInfraGeneric()){
2619
            inferredSynName.setInfraGenericEpithet(synParentInfragenericName);
2620
        }
2621

    
2622

    
2623
        potentialCombination = Synonym.NewInstance(inferredSynName, null);
2624

    
2625
        // Set the sourceReference
2626
        potentialCombination.setSec(sourceReference);
2627

    
2628

    
2629
        // Determine the idInSource
2630
        String idInSourceSyn= getIdInSource(syn);
2631

    
2632
        if (idInSourceParent != null && idInSourceSyn != null) {
2633
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2634
            inferredSynName.addSource(originalSource);
2635
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2636
            potentialCombination.addSource(originalSource);
2637
        }
2638

    
2639
        return potentialCombination;
2640
    }
2641

    
2642
    private Synonym createInferredGenus(Taxon taxon,
2643
            HashMap<UUID, ZoologicalName> zooHashMap, ZoologicalName taxonName,
2644
            String epithetOfTaxon, String genusOfTaxon,
2645
            List<String> taxonNames, ZoologicalName zooParentName,
2646
            TaxonBase syn) {
2647

    
2648
        Synonym inferredGenus;
2649
        TaxonNameBase synName;
2650
        ZoologicalName inferredSynName;
2651
        synName =syn.getName();
2652
        HibernateProxyHelper.deproxy(syn);
2653

    
2654
        // Determine the idInSource
2655
        String idInSourceSyn = getIdInSource(syn);
2656
        String idInSourceTaxon = getIdInSource(taxon);
2657
        // Determine the sourceReference
2658
        Reference sourceReference = syn.getSec();
2659

    
2660
        //logger.warn(sourceReference.getTitleCache());
2661

    
2662
        synName = syn.getName();
2663
        ZoologicalName synZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2664
        String synSpeciesEpithetName = synZooName.getSpecificEpithet();
2665
                     /* if (synonymsEpithet != null && !synonymsEpithet.contains(synSpeciesEpithetName)){
2666
            synonymsEpithet.add(synSpeciesEpithetName);
2667
        }*/
2668

    
2669
        inferredSynName = ZoologicalName.NewInstance(taxon.getName().getRank());
2670
        //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...
2671

    
2672

    
2673
        inferredSynName.setGenusOrUninomial(genusOfTaxon);
2674
        if (zooParentName.isInfraGeneric()){
2675
            inferredSynName.setInfraGenericEpithet(zooParentName.getInfraGenericEpithet());
2676
        }
2677

    
2678
        if (taxonName.isSpecies()){
2679
            inferredSynName.setSpecificEpithet(synSpeciesEpithetName);
2680
        }
2681
        if (taxonName.isInfraSpecific()){
2682
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2683
            inferredSynName.setInfraSpecificEpithet(synZooName.getInfraGenericEpithet());
2684
        }
2685

    
2686

    
2687
        inferredGenus = Synonym.NewInstance(inferredSynName, null);
2688

    
2689
        // Set the sourceReference
2690
        inferredGenus.setSec(sourceReference);
2691

    
2692
        // Add the original source
2693
        if (idInSourceSyn != null && idInSourceTaxon != null) {
2694
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2695
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2696
            inferredGenus.addSource(originalSource);
2697

    
2698
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2699
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2700
            inferredSynName.addSource(originalSource);
2701
            originalSource = null;
2702

    
2703
        }else{
2704
            logger.error("There is an idInSource missing: " + idInSourceSyn + " of Synonym or " + idInSourceTaxon + " of Taxon");
2705
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2706
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2707
            inferredGenus.addSource(originalSource);
2708

    
2709
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2710
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2711
            inferredSynName.addSource(originalSource);
2712
            originalSource = null;
2713
        }
2714

    
2715
        taxon.addSynonym(inferredGenus, SynonymRelationshipType.INFERRED_GENUS_OF());
2716

    
2717
        return inferredGenus;
2718
    }
2719

    
2720
    private Synonym createInferredEpithets(Taxon taxon,
2721
            HashMap<UUID, ZoologicalName> zooHashMap, ZoologicalName taxonName,
2722
            String epithetOfTaxon, String infragenericEpithetOfTaxon,
2723
            String infraspecificEpithetOfTaxon, List<String> taxonNames,
2724
            TaxonNameBase parentName, TaxonBase syn) {
2725

    
2726
        Synonym inferredEpithet;
2727
        TaxonNameBase<?,?> synName;
2728
        ZoologicalName inferredSynName;
2729
        HibernateProxyHelper.deproxy(syn);
2730

    
2731
        // Determine the idInSource
2732
        String idInSourceSyn = getIdInSource(syn);
2733
        String idInSourceTaxon =  getIdInSource(taxon);
2734
        // Determine the sourceReference
2735
        Reference sourceReference = syn.getSec();
2736

    
2737
        if (sourceReference == null){
2738
             logger.warn("The synonym has no sec reference because it is a misapplied name! Take the sec reference of taxon" + taxon.getSec());
2739
             sourceReference = taxon.getSec();
2740
        }
2741

    
2742
        synName = syn.getName();
2743
        ZoologicalName zooSynName = getZoologicalName(synName.getUuid(), zooHashMap);
2744
        String synGenusName = zooSynName.getGenusOrUninomial();
2745
        String synInfraGenericEpithet = null;
2746
        String synSpecificEpithet = null;
2747

    
2748
        if (zooSynName.getInfraGenericEpithet() != null){
2749
            synInfraGenericEpithet = zooSynName.getInfraGenericEpithet();
2750
        }
2751

    
2752
        if (zooSynName.isInfraSpecific()){
2753
            synSpecificEpithet = zooSynName.getSpecificEpithet();
2754
        }
2755

    
2756
                     /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2757
            synonymsGenus.put(synGenusName, idInSource);
2758
        }*/
2759

    
2760
        inferredSynName = ZoologicalName.NewInstance(taxon.getName().getRank());
2761

    
2762
        // DEBUG TODO: for subgenus or subspecies the infrageneric or infraspecific epithet should be used!!!
2763
        if (epithetOfTaxon == null && infragenericEpithetOfTaxon == null && infraspecificEpithetOfTaxon == null) {
2764
            logger.error("This specificEpithet is NULL" + taxon.getTitleCache());
2765
        }
2766
        inferredSynName.setGenusOrUninomial(synGenusName);
2767

    
2768
        if (parentName.isInfraGeneric()){
2769
            inferredSynName.setInfraGenericEpithet(synInfraGenericEpithet);
2770
        }
2771
        if (taxonName.isSpecies()){
2772
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2773
        }else if (taxonName.isInfraSpecific()){
2774
            inferredSynName.setSpecificEpithet(synSpecificEpithet);
2775
            inferredSynName.setInfraSpecificEpithet(infraspecificEpithetOfTaxon);
2776
        }
2777

    
2778
        inferredEpithet = Synonym.NewInstance(inferredSynName, null);
2779

    
2780
        // Set the sourceReference
2781
        inferredEpithet.setSec(sourceReference);
2782

    
2783
        /* Add the original source
2784
        if (idInSource != null) {
2785
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(idInSource, "InferredEpithetOf", syn.getSec(), null);
2786

    
2787
            // Add the citation
2788
            Reference citation = getCitation(syn);
2789
            if (citation != null) {
2790
                originalSource.setCitation(citation);
2791
                inferredEpithet.addSource(originalSource);
2792
            }
2793
        }*/
2794
        String taxonId = idInSourceTaxon+ "; " + idInSourceSyn;
2795

    
2796

    
2797
        IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2798
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2799

    
2800
        inferredEpithet.addSource(originalSource);
2801

    
2802
        originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2803
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2804

    
2805
        inferredSynName.addSource(originalSource);
2806

    
2807

    
2808

    
2809
        taxon.addSynonym(inferredEpithet, SynonymRelationshipType.INFERRED_EPITHET_OF());
2810

    
2811
        return inferredEpithet;
2812
    }
2813

    
2814
    /**
2815
     * Returns an existing ZoologicalName or extends an internal hashmap if it does not exist.
2816
     * Very likely only useful for createInferredSynonyms().
2817
     * @param uuid
2818
     * @param zooHashMap
2819
     * @return
2820
     */
2821
    private ZoologicalName getZoologicalName(UUID uuid, HashMap <UUID, ZoologicalName> zooHashMap) {
2822
        ZoologicalName taxonName =nameDao.findZoologicalNameByUUID(uuid);
2823
        if (taxonName == null) {
2824
            taxonName = zooHashMap.get(uuid);
2825
        }
2826
        return taxonName;
2827
    }
2828

    
2829
    /**
2830
     * Returns the idInSource for a given Synonym.
2831
     * @param syn
2832
     */
2833
    private String getIdInSource(TaxonBase taxonBase) {
2834
        String idInSource = null;
2835
        Set<IdentifiableSource> sources = taxonBase.getSources();
2836
        if (sources.size() == 1) {
2837
            IdentifiableSource source = sources.iterator().next();
2838
            if (source != null) {
2839
                idInSource  = source.getIdInSource();
2840
            }
2841
        } else if (sources.size() > 1) {
2842
            int count = 1;
2843
            idInSource = "";
2844
            for (IdentifiableSource source : sources) {
2845
                idInSource += source.getIdInSource();
2846
                if (count < sources.size()) {
2847
                    idInSource += "; ";
2848
                }
2849
                count++;
2850
            }
2851
        } else if (sources.size() == 0){
2852
            logger.warn("No idInSource for TaxonBase " + taxonBase.getUuid() + " - " + taxonBase.getTitleCache());
2853
        }
2854

    
2855

    
2856
        return idInSource;
2857
    }
2858

    
2859

    
2860
    /**
2861
     * Returns the citation for a given Synonym.
2862
     * @param syn
2863
     */
2864
    private Reference getCitation(Synonym syn) {
2865
        Reference citation = null;
2866
        Set<IdentifiableSource> sources = syn.getSources();
2867
        if (sources.size() == 1) {
2868
            IdentifiableSource source = sources.iterator().next();
2869
            if (source != null) {
2870
                citation = source.getCitation();
2871
            }
2872
        } else if (sources.size() > 1) {
2873
            logger.warn("This Synonym has more than one source: " + syn.getUuid() + " (" + syn.getTitleCache() +")");
2874
        }
2875

    
2876
        return citation;
2877
    }
2878

    
2879
    @Override
2880
    public List<Synonym>  createAllInferredSynonyms(Taxon taxon, Classification tree, boolean doWithMisappliedNames){
2881
        List <Synonym> inferredSynonyms = new ArrayList<Synonym>();
2882

    
2883
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymRelationshipType.INFERRED_EPITHET_OF(), doWithMisappliedNames));
2884
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymRelationshipType.INFERRED_GENUS_OF(), doWithMisappliedNames));
2885
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymRelationshipType.POTENTIAL_COMBINATION_OF(), doWithMisappliedNames));
2886

    
2887
        return inferredSynonyms;
2888
    }
2889

    
2890
    @Override
2891
    public List<Classification> listClassifications(TaxonBase taxonBase, Integer limit, Integer start, List<String> propertyPaths) {
2892

    
2893
        // TODO quickly implemented, create according dao !!!!
2894
        Set<TaxonNode> nodes = new HashSet<TaxonNode>();
2895
        Set<Classification> classifications = new HashSet<Classification>();
2896
        List<Classification> list = new ArrayList<Classification>();
2897

    
2898
        if (taxonBase == null) {
2899
            return list;
2900
        }
2901

    
2902
        taxonBase = load(taxonBase.getUuid());
2903

    
2904
        if (taxonBase instanceof Taxon) {
2905
            nodes.addAll(((Taxon)taxonBase).getTaxonNodes());
2906
        } else {
2907
            for (Taxon taxon : ((Synonym)taxonBase).getAcceptedTaxa() ) {
2908
                nodes.addAll(taxon.getTaxonNodes());
2909
            }
2910
        }
2911
        for (TaxonNode node : nodes) {
2912
            classifications.add(node.getClassification());
2913
        }
2914
        list.addAll(classifications);
2915
        return list;
2916
    }
2917

    
2918
    @Override
2919
    @Transactional(readOnly = false)
2920
    public UpdateResult changeRelatedTaxonToSynonym(UUID fromTaxonUuid,
2921
            UUID toTaxonUuid,
2922
            TaxonRelationshipType oldRelationshipType,
2923
            SynonymRelationshipType synonymRelationshipType) throws DataChangeNoRollbackException {
2924
        UpdateResult result = new UpdateResult();
2925
        Taxon fromTaxon = (Taxon) dao.load(fromTaxonUuid);
2926
        Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
2927
        Synonym synonym = changeRelatedTaxonToSynonym(fromTaxon, toTaxon, oldRelationshipType, synonymRelationshipType);
2928
        result.setCdmEntity(synonym);
2929
        result.addUpdatedObject(fromTaxon);
2930
        result.addUpdatedObject(toTaxon);
2931
        result.addUpdatedObject(synonym);
2932

    
2933
        return result;
2934
    }
2935

    
2936
    @Override
2937
    @Transactional(readOnly = false)
2938
    public Synonym changeRelatedTaxonToSynonym(Taxon fromTaxon, Taxon toTaxon, TaxonRelationshipType oldRelationshipType,
2939
            SynonymRelationshipType synonymRelationshipType) throws DataChangeNoRollbackException {
2940
        // Create new synonym using concept name
2941
                TaxonNameBase<?, ?> synonymName = fromTaxon.getName();
2942
                Synonym synonym = Synonym.NewInstance(synonymName, fromTaxon.getSec());
2943

    
2944
                // Remove concept relation from taxon
2945
                toTaxon.removeTaxon(fromTaxon, oldRelationshipType);
2946

    
2947

    
2948

    
2949

    
2950
                // Create a new synonym for the taxon
2951
                SynonymRelationship synonymRelationship;
2952
                if (synonymRelationshipType != null
2953
                        && synonymRelationshipType.equals(SynonymRelationshipType.HOMOTYPIC_SYNONYM_OF())){
2954
                    synonymRelationship = toTaxon.addHomotypicSynonym(synonym, null, null);
2955
                } else{
2956
                    synonymRelationship = toTaxon.addHeterotypicSynonymName(synonymName);
2957
                }
2958

    
2959
                this.saveOrUpdate(toTaxon);
2960
                //TODO: configurator and classification
2961
                TaxonDeletionConfigurator config = new TaxonDeletionConfigurator();
2962
                config.setDeleteNameIfPossible(false);
2963
                this.deleteTaxon(fromTaxon.getUuid(), config, null);
2964
                return synonymRelationship.getSynonym();
2965

    
2966
    }
2967

    
2968
    @Override
2969
    public DeleteResult isDeletable(TaxonBase taxonBase, DeleteConfiguratorBase config){
2970
        DeleteResult result = new DeleteResult();
2971
        Set<CdmBase> references = commonService.getReferencingObjectsForDeletion(taxonBase);
2972
        if (taxonBase instanceof Taxon){
2973
            TaxonDeletionConfigurator taxonConfig = (TaxonDeletionConfigurator) config;
2974
            result = isDeletableForTaxon(references, taxonConfig);
2975
        }else{
2976
            SynonymDeletionConfigurator synonymConfig = (SynonymDeletionConfigurator) config;
2977
            result = isDeletableForSynonym(references, synonymConfig);
2978
        }
2979
        return result;
2980
    }
2981

    
2982
    private DeleteResult isDeletableForSynonym(Set<CdmBase> references, SynonymDeletionConfigurator config){
2983
        String message;
2984
        DeleteResult result = new DeleteResult();
2985
        for (CdmBase ref: references){
2986
            if (!(ref instanceof SynonymRelationship || ref instanceof Taxon || ref instanceof TaxonNameBase )){
2987
                message = "The Synonym can't be deleted as long as it is referenced by " + ref.getClass().getSimpleName() + " with id "+ ref.getId();
2988
                result.addException(new ReferencedObjectUndeletableException(message));
2989
                result.addRelatedObject(ref);
2990
                result.setAbort();
2991
            }
2992
        }
2993

    
2994
        return result;
2995
    }
2996

    
2997
    private DeleteResult isDeletableForTaxon(Set<CdmBase> references, TaxonDeletionConfigurator config){
2998
        String message = null;
2999
        DeleteResult result = new DeleteResult();
3000
        for (CdmBase ref: references){
3001
            if (!(ref instanceof TaxonNameBase)){
3002
            	message = null;
3003
                if (!config.isDeleteSynonymRelations() && (ref instanceof SynonymRelationship)){
3004
                    message = "The taxon can't be deleted as long as it has synonyms.";
3005

    
3006
                }
3007
                if (!config.isDeleteDescriptions() && (ref instanceof DescriptionBase)){
3008
                    message = "The taxon can't be deleted as long as it has factual data.";
3009

    
3010
                }
3011

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

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

    
3020
                    } else{
3021
                        message = "The taxon can't be deleted as long as it belongs to a taxon node.";
3022

    
3023
                    }
3024
                }
3025
                if (ref instanceof PolytomousKeyNode){
3026
                    message = "The taxon can't be deleted as long as it is referenced by a polytomous key node.";
3027

    
3028
                }
3029

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

    
3033

    
3034
                }
3035

    
3036

    
3037
               /* //PolytomousKeyNode
3038
                if (referencingObject.isInstanceOf(PolytomousKeyNode.class)){
3039
                    String message = "Taxon" + taxon.getTitleCache() + " can't be deleted as it is used in polytomous key node";
3040
                    return message;
3041
                }*/
3042

    
3043
                //TaxonInteraction
3044
                if (ref.isInstanceOf(TaxonInteraction.class)){
3045
                    message = "Taxon can't be deleted as it is used in taxonInteraction#taxon2";
3046

    
3047
                }
3048

    
3049
              //TaxonInteraction
3050
                if (ref.isInstanceOf(DeterminationEvent.class)){
3051
                    message = "Taxon can't be deleted as it is used in a determination event";
3052

    
3053
                }
3054

    
3055
            }
3056
            if (message != null){
3057
	            result.addException(new ReferencedObjectUndeletableException(message));
3058
	            result.addRelatedObject(ref);
3059
	            result.setAbort();
3060
            }
3061
        }
3062

    
3063
        return result;
3064
    }
3065

    
3066
    @Override
3067
    public IncludedTaxaDTO listIncludedTaxa(UUID taxonUuid, IncludedTaxonConfiguration config) {
3068
        IncludedTaxaDTO result = new IncludedTaxaDTO(taxonUuid);
3069

    
3070
        //preliminary implementation
3071

    
3072
        Set<Taxon> taxa = new HashSet<Taxon>();
3073
        TaxonBase taxonBase = find(taxonUuid);
3074
        if (taxonBase == null){
3075
            return new IncludedTaxaDTO();
3076
        }else if (taxonBase.isInstanceOf(Taxon.class)){
3077
            Taxon taxon = CdmBase.deproxy(taxonBase, Taxon.class);
3078
            taxa.add(taxon);
3079
        }else if (taxonBase.isInstanceOf(Synonym.class)){
3080
            //TODO partial synonyms ??
3081
            //TODO synonyms in general
3082
            Synonym syn = CdmBase.deproxy(taxonBase, Synonym.class);
3083
            taxa.addAll(syn.getAcceptedTaxa());
3084
        }else{
3085
            throw new IllegalArgumentException("Unhandled class " + taxonBase.getClass().getSimpleName());
3086
        }
3087

    
3088
        Set<Taxon> related = makeRelatedIncluded(taxa, result, config);
3089
        int i = 0;
3090
        while((! related.isEmpty()) && i++ < 100){  //to avoid
3091
             related = makeRelatedIncluded(related, result, config);
3092
        }
3093

    
3094
        return result;
3095
    }
3096

    
3097
    /**
3098
     * Computes all children and conceptually congruent and included taxa and adds them to the existingTaxa
3099
     * data structure.
3100
     * @return the set of conceptually related taxa for further use
3101
     */
3102
    /**
3103
     * @param uncheckedTaxa
3104
     * @param existingTaxa
3105
     * @param config
3106
     * @return
3107
     */
3108
    private Set<Taxon> makeRelatedIncluded(Set<Taxon> uncheckedTaxa, IncludedTaxaDTO existingTaxa, IncludedTaxonConfiguration config) {
3109

    
3110
        //children
3111
        Set<TaxonNode> taxonNodes = new HashSet<TaxonNode>();
3112
        for (Taxon taxon: uncheckedTaxa){
3113
            taxonNodes.addAll(taxon.getTaxonNodes());
3114
        }
3115

    
3116
        Set<Taxon> children = new HashSet<Taxon>();
3117
        if (! config.onlyCongruent){
3118
            for (TaxonNode node: taxonNodes){
3119
                List<TaxonNode> childNodes = nodeService.loadChildNodesOfTaxonNode(node, null, true, null);
3120
                for (TaxonNode child : childNodes){
3121
                    children.add(child.getTaxon());
3122
                }
3123
            }
3124
            children.remove(null);  // just to be on the save side
3125
        }
3126

    
3127
        Iterator<Taxon> it = children.iterator();
3128
        while(it.hasNext()){
3129
            UUID uuid = it.next().getUuid();
3130
            if (existingTaxa.contains(uuid)){
3131
                it.remove();
3132
            }else{
3133
                existingTaxa.addIncludedTaxon(uuid, new ArrayList<UUID>(), false);
3134
            }
3135
        }
3136

    
3137
        //concept relations
3138
        Set<Taxon> uncheckedAndChildren = new HashSet<Taxon>(uncheckedTaxa);
3139
        uncheckedAndChildren.addAll(children);
3140

    
3141
        Set<Taxon> relatedTaxa = makeConceptIncludedTaxa(uncheckedAndChildren, existingTaxa, config);
3142

    
3143

    
3144
        Set<Taxon> result = new HashSet<Taxon>(relatedTaxa);
3145
        return result;
3146
    }
3147

    
3148
    /**
3149
     * Computes all conceptually congruent or included taxa and adds them to the existingTaxa data structure.
3150
     * @return the set of these computed taxa
3151
     */
3152
    private Set<Taxon> makeConceptIncludedTaxa(Set<Taxon> unchecked, IncludedTaxaDTO existingTaxa, IncludedTaxonConfiguration config) {
3153
        Set<Taxon> result = new HashSet<Taxon>();
3154

    
3155
        for (Taxon taxon : unchecked){
3156
            Set<TaxonRelationship> fromRelations = taxon.getRelationsFromThisTaxon();
3157
            Set<TaxonRelationship> toRelations = taxon.getRelationsToThisTaxon();
3158

    
3159
            for (TaxonRelationship fromRel : fromRelations){
3160
                if (config.includeDoubtful == false && fromRel.isDoubtful()){
3161
                    continue;
3162
                }
3163
                if (fromRel.getType().equals(TaxonRelationshipType.CONGRUENT_TO()) ||
3164
                        !config.onlyCongruent && fromRel.getType().equals(TaxonRelationshipType.INCLUDES()) ||
3165
                        !config.onlyCongruent && fromRel.getType().equals(TaxonRelationshipType.CONGRUENT_OR_INCLUDES())
3166
                        ){
3167
                    result.add(fromRel.getToTaxon());
3168
                }
3169
            }
3170

    
3171
            for (TaxonRelationship toRel : toRelations){
3172
                if (config.includeDoubtful == false && toRel.isDoubtful()){
3173
                    continue;
3174
                }
3175
                if (toRel.getType().equals(TaxonRelationshipType.CONGRUENT_TO())){
3176
                    result.add(toRel.getFromTaxon());
3177
                }
3178
            }
3179
        }
3180

    
3181
        Iterator<Taxon> it = result.iterator();
3182
        while(it.hasNext()){
3183
            UUID uuid = it.next().getUuid();
3184
            if (existingTaxa.contains(uuid)){
3185
                it.remove();
3186
            }else{
3187
                existingTaxa.addIncludedTaxon(uuid, new ArrayList<UUID>(), false);
3188
            }
3189
        }
3190
        return result;
3191
    }
3192

    
3193
    @Override
3194
    public List<TaxonBase> findTaxaByName(MatchingTaxonConfigurator config){
3195
        List<TaxonBase> taxonList = dao.getTaxaByName(true, false, false, config.getTaxonNameTitle(), null, MatchMode.EXACT, null, 0, 0, config.getPropertyPath());
3196
        return taxonList;
3197
    }
3198

    
3199
	@Override
3200
	@Transactional(readOnly = true)
3201
	public <S extends TaxonBase> Pager<FindByIdentifierDTO<S>> findByIdentifier(
3202
			Class<S> clazz, String identifier, DefinedTerm identifierType, TaxonNode subtreeFilter,
3203
			MatchMode matchmode, boolean includeEntity, Integer pageSize,
3204
			Integer pageNumber,	List<String> propertyPaths) {
3205
		if (subtreeFilter == null){
3206
			return findByIdentifier(clazz, identifier, identifierType, matchmode, includeEntity, pageSize, pageNumber, propertyPaths);
3207
		}
3208

    
3209
		Integer numberOfResults = dao.countByIdentifier(clazz, identifier, identifierType, subtreeFilter, matchmode);
3210
        List<Object[]> daoResults = new ArrayList<Object[]>();
3211
        if(numberOfResults > 0) { // no point checking again
3212
        	daoResults = dao.findByIdentifier(clazz, identifier, identifierType, subtreeFilter,
3213
    				matchmode, includeEntity, pageSize, pageNumber, propertyPaths);
3214
        }
3215

    
3216
        List<FindByIdentifierDTO<S>> result = new ArrayList<FindByIdentifierDTO<S>>();
3217
        for (Object[] daoObj : daoResults){
3218
        	if (includeEntity){
3219
        		result.add(new FindByIdentifierDTO<S>((DefinedTerm)daoObj[0], (String)daoObj[1], (S)daoObj[2]));
3220
        	}else{
3221
        		result.add(new FindByIdentifierDTO<S>((DefinedTerm)daoObj[0], (String)daoObj[1], (UUID)daoObj[2], (String)daoObj[3]));
3222
        	}
3223
        }
3224
		return new DefaultPagerImpl<FindByIdentifierDTO<S>>(pageNumber, numberOfResults, pageSize, result);
3225
	}
3226

    
3227
	@Override
3228
	@Transactional(readOnly = false)
3229
	public UpdateResult moveSynonymToAnotherTaxon(SynonymRelationship oldSynonymRelation, UUID newTaxonUUID, boolean moveHomotypicGroup,
3230
            SynonymRelationshipType newSynonymRelationshipType, Reference reference, String referenceDetail, boolean keepReference) throws HomotypicalGroupChangeException {
3231

    
3232
	    UpdateResult result = new UpdateResult();
3233
		Taxon newTaxon = (Taxon) dao.load(newTaxonUUID);
3234
		result = moveSynonymToAnotherTaxon(oldSynonymRelation, newTaxon, moveHomotypicGroup, newSynonymRelationshipType, reference, referenceDetail, keepReference);
3235

    
3236
		return result;
3237
	}
3238

    
3239
	@Override
3240
	public UpdateResult moveFactualDateToAnotherTaxon(UUID fromTaxonUuid, UUID toTaxonUuid){
3241
		UpdateResult result = new UpdateResult();
3242

    
3243
		Taxon fromTaxon = (Taxon)dao.load(fromTaxonUuid);
3244
		Taxon toTaxon = (Taxon) dao.load(toTaxonUuid);
3245
		  for(TaxonDescription description : fromTaxon.getDescriptions()){
3246
              //reload to avoid session conflicts
3247
              description = HibernateProxyHelper.deproxy(description, TaxonDescription.class);
3248

    
3249
              String moveMessage = String.format("Description moved from %s", fromTaxon);
3250
              if(description.isProtectedTitleCache()){
3251
                  String separator = "";
3252
                  if(!StringUtils.isBlank(description.getTitleCache())){
3253
                      separator = " - ";
3254
                  }
3255
                  description.setTitleCache(description.getTitleCache() + separator + moveMessage, true);
3256
              }
3257
              Annotation annotation = Annotation.NewInstance(moveMessage, Language.getDefaultLanguage());
3258
              annotation.setAnnotationType(AnnotationType.TECHNICAL());
3259
              description.addAnnotation(annotation);
3260
              toTaxon.addDescription(description);
3261
              dao.saveOrUpdate(toTaxon);
3262
              dao.saveOrUpdate(fromTaxon);
3263
              result.addUpdatedObject(toTaxon);
3264
              result.addUpdatedObject(fromTaxon);
3265

    
3266
          }
3267

    
3268

    
3269
		return result;
3270
	}
3271

    
3272
	@Override
3273
	public DeleteResult deleteSynonym(UUID synonymUuid, UUID taxonUuid,
3274
			SynonymDeletionConfigurator config) {
3275
		TaxonBase base = this.load(synonymUuid);
3276
		Synonym syn = HibernateProxyHelper.deproxy(base, Synonym.class);
3277
		base = this.load(taxonUuid);
3278
		Taxon taxon = HibernateProxyHelper.deproxy(base, Taxon.class);
3279

    
3280
		return this.deleteSynonym(syn, taxon, config);
3281
	}
3282

    
3283
	@Override
3284
	@Transactional(readOnly = false)
3285
	public UpdateResult swapSynonymAndAcceptedTaxon(UUID synonymUUid,
3286
			UUID acceptedTaxonUuid) {
3287
		TaxonBase base = this.load(synonymUUid);
3288
		Synonym syn = HibernateProxyHelper.deproxy(base, Synonym.class);
3289
		base = this.load(acceptedTaxonUuid);
3290
		Taxon taxon = HibernateProxyHelper.deproxy(base, Taxon.class);
3291

    
3292
		return this.swapSynonymAndAcceptedTaxon(syn, taxon);
3293
	}
3294

    
3295
	@Override
3296
	public UUID saveOrUpdate(TaxonBase taxonbase){
3297
	    if (taxonbase.getName()!= null && taxonbase.getName().getId() > 0){
3298
	        TaxonNameBase name = taxonbase.getName();
3299
	        name = nameService.load(name.getUuid());
3300
	        taxonbase.setName(name);
3301
	    }
3302
	    return super.saveOrUpdate(taxonbase);
3303
	}
3304

    
3305

    
3306

    
3307
}
(90-90/98)