Project

General

Profile

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

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

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

    
23
import javax.persistence.EntityNotFoundException;
24

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

    
40
import eu.etaxonomy.cdm.api.service.config.DeleteConfiguratorBase;
41
import eu.etaxonomy.cdm.api.service.config.IFindTaxaAndNamesConfigurator;
42
import eu.etaxonomy.cdm.api.service.config.IncludedTaxonConfiguration;
43
import eu.etaxonomy.cdm.api.service.config.MatchingTaxonConfigurator;
44
import eu.etaxonomy.cdm.api.service.config.NodeDeletionConfigurator.ChildHandling;
45
import eu.etaxonomy.cdm.api.service.config.SynonymDeletionConfigurator;
46
import eu.etaxonomy.cdm.api.service.config.TaxonDeletionConfigurator;
47
import eu.etaxonomy.cdm.api.service.dto.IdentifiedEntityDTO;
48
import eu.etaxonomy.cdm.api.service.dto.IncludedTaxaDTO;
49
import eu.etaxonomy.cdm.api.service.dto.MarkedEntityDTO;
50
import eu.etaxonomy.cdm.api.service.dto.TaxonRelationshipsDTO;
51
import eu.etaxonomy.cdm.api.service.exception.DataChangeNoRollbackException;
52
import eu.etaxonomy.cdm.api.service.exception.HomotypicalGroupChangeException;
53
import eu.etaxonomy.cdm.api.service.exception.ReferencedObjectUndeletableException;
54
import eu.etaxonomy.cdm.api.service.pager.Pager;
55
import eu.etaxonomy.cdm.api.service.pager.impl.DefaultPagerImpl;
56
import eu.etaxonomy.cdm.api.service.search.ILuceneIndexToolProvider;
57
import eu.etaxonomy.cdm.api.service.search.ISearchResultBuilder;
58
import eu.etaxonomy.cdm.api.service.search.LuceneMultiSearch;
59
import eu.etaxonomy.cdm.api.service.search.LuceneMultiSearchException;
60
import eu.etaxonomy.cdm.api.service.search.LuceneParseException;
61
import eu.etaxonomy.cdm.api.service.search.LuceneSearch;
62
import eu.etaxonomy.cdm.api.service.search.QueryFactory;
63
import eu.etaxonomy.cdm.api.service.search.SearchResult;
64
import eu.etaxonomy.cdm.api.service.search.SearchResultBuilder;
65
import eu.etaxonomy.cdm.api.service.util.TaxonRelationshipEdge;
66
import eu.etaxonomy.cdm.common.monitor.IProgressMonitor;
67
import eu.etaxonomy.cdm.exception.UnpublishedException;
68
import eu.etaxonomy.cdm.hibernate.HibernateProxyHelper;
69
import eu.etaxonomy.cdm.hibernate.search.AcceptedTaxonBridge;
70
import eu.etaxonomy.cdm.hibernate.search.GroupByTaxonClassBridge;
71
import eu.etaxonomy.cdm.model.CdmBaseType;
72
import eu.etaxonomy.cdm.model.common.Annotation;
73
import eu.etaxonomy.cdm.model.common.AnnotationType;
74
import eu.etaxonomy.cdm.model.common.CdmBase;
75
import eu.etaxonomy.cdm.model.common.DefinedTerm;
76
import eu.etaxonomy.cdm.model.common.IdentifiableEntity;
77
import eu.etaxonomy.cdm.model.common.IdentifiableSource;
78
import eu.etaxonomy.cdm.model.common.Language;
79
import eu.etaxonomy.cdm.model.common.MarkerType;
80
import eu.etaxonomy.cdm.model.common.OriginalSourceType;
81
import eu.etaxonomy.cdm.model.common.RelationshipBase.Direction;
82
import eu.etaxonomy.cdm.model.description.CommonTaxonName;
83
import eu.etaxonomy.cdm.model.description.DescriptionBase;
84
import eu.etaxonomy.cdm.model.description.DescriptionElementBase;
85
import eu.etaxonomy.cdm.model.description.Distribution;
86
import eu.etaxonomy.cdm.model.description.Feature;
87
import eu.etaxonomy.cdm.model.description.IIdentificationKey;
88
import eu.etaxonomy.cdm.model.description.PolytomousKeyNode;
89
import eu.etaxonomy.cdm.model.description.PresenceAbsenceTerm;
90
import eu.etaxonomy.cdm.model.description.SpecimenDescription;
91
import eu.etaxonomy.cdm.model.description.TaxonDescription;
92
import eu.etaxonomy.cdm.model.description.TaxonInteraction;
93
import eu.etaxonomy.cdm.model.description.TaxonNameDescription;
94
import eu.etaxonomy.cdm.model.location.NamedArea;
95
import eu.etaxonomy.cdm.model.media.Media;
96
import eu.etaxonomy.cdm.model.name.HomotypicalGroup;
97
import eu.etaxonomy.cdm.model.name.IZoologicalName;
98
import eu.etaxonomy.cdm.model.name.Rank;
99
import eu.etaxonomy.cdm.model.name.TaxonName;
100
import eu.etaxonomy.cdm.model.name.TaxonNameFactory;
101
import eu.etaxonomy.cdm.model.occurrence.DerivedUnit;
102
import eu.etaxonomy.cdm.model.occurrence.DeterminationEvent;
103
import eu.etaxonomy.cdm.model.occurrence.SpecimenOrObservationBase;
104
import eu.etaxonomy.cdm.model.reference.Reference;
105
import eu.etaxonomy.cdm.model.taxon.Classification;
106
import eu.etaxonomy.cdm.model.taxon.HomotypicGroupTaxonComparator;
107
import eu.etaxonomy.cdm.model.taxon.ITaxonTreeNode;
108
import eu.etaxonomy.cdm.model.taxon.Synonym;
109
import eu.etaxonomy.cdm.model.taxon.SynonymType;
110
import eu.etaxonomy.cdm.model.taxon.Taxon;
111
import eu.etaxonomy.cdm.model.taxon.TaxonBase;
112
import eu.etaxonomy.cdm.model.taxon.TaxonNode;
113
import eu.etaxonomy.cdm.model.taxon.TaxonRelationship;
114
import eu.etaxonomy.cdm.model.taxon.TaxonRelationshipType;
115
import eu.etaxonomy.cdm.persistence.dao.initializer.AbstractBeanInitializer;
116
import eu.etaxonomy.cdm.persistence.dao.name.ITaxonNameDao;
117
import eu.etaxonomy.cdm.persistence.dao.occurrence.IOccurrenceDao;
118
import eu.etaxonomy.cdm.persistence.dao.taxon.IClassificationDao;
119
import eu.etaxonomy.cdm.persistence.dao.taxon.ITaxonDao;
120
import eu.etaxonomy.cdm.persistence.dao.taxon.ITaxonNodeDao;
121
import eu.etaxonomy.cdm.persistence.dto.UuidAndTitleCache;
122
import eu.etaxonomy.cdm.persistence.query.MatchMode;
123
import eu.etaxonomy.cdm.persistence.query.OrderHint;
124
import eu.etaxonomy.cdm.persistence.query.OrderHint.SortOrder;
125
import eu.etaxonomy.cdm.persistence.query.TaxonTitleType;
126
import eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy;
127

    
128

    
129
/**
130
 * @author a.kohlbecker
131
 * @since 10.09.2010
132
 */
133
@Service
134
@Transactional(readOnly = true)
135
public class TaxonServiceImpl
136
            extends IdentifiableServiceBase<TaxonBase,ITaxonDao>
137
            implements ITaxonService{
138

    
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 IDescriptionService descriptionService;
164
//
165
//    @Autowired
166
//    private IOrderedTermVocabularyDao orderedVocabularyDao;
167

    
168
    @Autowired
169
    private IOccurrenceDao occurrenceDao;
170

    
171
    @Autowired
172
    private IClassificationDao classificationDao;
173

    
174
    @Autowired
175
    private AbstractBeanInitializer beanInitializer;
176

    
177
    @Autowired
178
    private ILuceneIndexToolProvider luceneIndexToolProvider;
179

    
180
//************************ CONSTRUCTOR ****************************/
181
    public TaxonServiceImpl(){
182
        if (logger.isDebugEnabled()) { logger.debug("Load TaxonService Bean"); }
183
    }
184

    
185
// ****************************** METHODS ********************************/
186

    
187

    
188
    /**
189
     * {@inheritDoc}
190
     */
191
    @Override
192
    public TaxonBase load(UUID uuid, boolean includeUnpublished, List<String> propertyPaths) {
193
        return dao.load(uuid, includeUnpublished, propertyPaths);
194
    }
195

    
196
    @Override
197
    public List<TaxonBase> searchByName(String name, boolean includeUnpublished, Reference sec) {
198
        return dao.getTaxaByName(name, includeUnpublished, sec);
199
    }
200

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

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

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

    
227

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

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

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

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

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

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

    
273
        return result;
274
    }
275

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

    
296

    
297

    
298

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

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

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

    
326
        UpdateResult result = new UpdateResult();
327

    
328
        TaxonName synonymName = synonym.getName();
329

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

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

    
346
        return result;
347
    }
348

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

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

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

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

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

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

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

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

    
395
    }
396

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

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

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

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

    
421
        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
422
    }
423

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

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

    
433
        return results;
434
    }
435

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

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

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

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

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

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

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

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

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

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

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

    
500
        Synonym synonym = null;
501

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

    
511
        Classification classificationFilter = null;
512
        if(classificationUuid != null){
513
            try {
514
                classificationFilter = classificationDao.load(classificationUuid);
515
            } catch (NullPointerException e){
516
                //TODO not sure, why an NPE should be thrown in the above load method
517
                throw new EntityNotFoundException("No Classification entity found for " + classificationUuid);
518
            }
519
            if(classificationFilter == null){
520
                throw new EntityNotFoundException("No Classification entity found for " + classificationUuid);
521
            }
522
        }
523

    
524
        long count = dao.countAcceptedTaxonFor(synonym, classificationFilter) ;
525
        if(count > 0){
526
            Taxon result = dao.acceptedTaxonFor(synonym, classificationFilter, propertyPaths);
527
            checkPublished(result, includeUnpublished, "Accepted taxon unpublished");
528
            return result;
529
        }else{
530
            return null;
531
        }
532
    }
533

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

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

    
544

    
545
    /**
546
     * Recursively collect related taxa for the given <code>taxon</code> . The returned list will also include the
547
     *  <code>taxon</code> supplied as parameter.
548
     *
549
     * @param taxon
550
     * @param includeRelationships
551
     * @param taxa
552
     * @param maxDepth can be <code>null</code> for infinite depth
553
     * @return
554
     */
555
    private Set<Taxon> collectRelatedTaxa(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, Set<Taxon> taxa,
556
            boolean includeUnpublished, 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,
573
                (Set<TaxonRelationshipType>)null, includeUnpublished, null, null, null, null, null);
574
        for (TaxonRelationship taxRel : taxonRelationships) {
575

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

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

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

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

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

    
625

    
626
        //homotypic
627
        result.add(taxon.getHomotypicSynonymsByHomotypicGroup(comparator));
628

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

    
635
        return result;
636

    
637
    }
638

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

    
644
        return t.getHomotypicSynonymsByHomotypicGroup(comparator);
645
    }
646

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

    
658
    @Override
659
    public List<UuidAndTitleCache<? extends IdentifiableEntity>> findTaxaAndNamesForEditor(IFindTaxaAndNamesConfigurator config){
660

    
661
        if (config.isDoSynonyms() || config.isDoTaxa() || config.isDoNamesWithoutTaxa() || config.isDoTaxaByCommonNames()){
662
        	return dao.getTaxaByNameForEditor(config.isDoTaxa(), config.isDoSynonyms(), config.isDoNamesWithoutTaxa(),
663
        	        config.isDoMisappliedNames(), config.isDoTaxaByCommonNames(), config.isIncludeUnpublished(),
664
        	        config.getTitleSearchStringSqlized(), config.getClassification(), config.getSubtree(),
665
        	        config.getMatchMode(), config.getNamedAreas(), config.getOrder());
666
        }else{
667
            return new ArrayList<>();
668
        }
669
    }
670

    
671
    @Override
672
    public Pager<IdentifiableEntity> findTaxaAndNames(IFindTaxaAndNamesConfigurator configurator) {
673

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

    
678
        // Taxa and synonyms
679
        long numberTaxaResults = 0L;
680

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

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

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

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

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

    
710
        numberOfResults += numberTaxaResults;
711

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

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

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

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

    
739
    @Override
740
    public List<Media> listTaxonDescriptionMedia(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships, boolean limitToGalleries, List<String> propertyPath){
741
        return listMedia(taxon, includeRelationships, limitToGalleries, true, false, false, propertyPath);
742
    }
743

    
744
    @Override
745
    public List<Media> listMedia(Taxon taxon, Set<TaxonRelationshipEdge> includeRelationships,
746
            Boolean limitToGalleries, Boolean includeTaxonDescriptions, Boolean includeOccurrences,
747
            Boolean includeTaxonNameDescriptions, List<String> propertyPath) {
748

    
749
        //TODO let inherit
750
        boolean includeUnpublished = INCLUDE_UNPUBLISHED;
751

    
752
    //    logger.setLevel(Level.TRACE);
753
//        Logger.getLogger("org.hibernate.SQL").setLevel(Level.TRACE);
754

    
755
        logger.trace("listMedia() - START");
756

    
757
        Set<Taxon> taxa = new HashSet<>();
758
        List<Media> taxonMedia = new ArrayList<>();
759
        List<Media> nonImageGalleryImages = new ArrayList<>();
760

    
761
        if (limitToGalleries == null) {
762
            limitToGalleries = false;
763
        }
764

    
765
        // --- resolve related taxa
766
        if (includeRelationships != null && ! includeRelationships.isEmpty()) {
767
            logger.trace("listMedia() - resolve related taxa");
768
            taxa = listRelatedTaxa(taxon, includeRelationships, null, includeUnpublished, null, null, null);
769
        }
770

    
771
        taxa.add((Taxon) dao.load(taxon.getUuid()));
772

    
773
        if(includeTaxonDescriptions != null && includeTaxonDescriptions){
774
            logger.trace("listMedia() - includeTaxonDescriptions");
775
            List<TaxonDescription> taxonDescriptions = new ArrayList<>();
776
            // --- TaxonDescriptions
777
            for (Taxon t : taxa) {
778
                taxonDescriptions.addAll(descriptionService.listTaxonDescriptions(t, null, null, null, null, propertyPath));
779
            }
780
            for (TaxonDescription taxonDescription : taxonDescriptions) {
781
                if (!limitToGalleries || taxonDescription.isImageGallery()) {
782
                    for (DescriptionElementBase element : taxonDescription.getElements()) {
783
                        for (Media media : element.getMedia()) {
784
                            if(taxonDescription.isImageGallery()){
785
                                taxonMedia.add(media);
786
                            }
787
                            else{
788
                                nonImageGalleryImages.add(media);
789
                            }
790
                        }
791
                    }
792
                }
793
            }
794
            //put images from image gallery first (#3242)
795
            taxonMedia.addAll(nonImageGalleryImages);
796
        }
797

    
798

    
799
        if(includeOccurrences != null && includeOccurrences) {
800
            logger.trace("listMedia() - includeOccurrences");
801
            Set<SpecimenOrObservationBase> specimensOrObservations = new HashSet<>();
802
            // --- Specimens
803
            for (Taxon t : taxa) {
804
                specimensOrObservations.addAll(occurrenceDao.listByAssociatedTaxon(null, t, null, null, null, null));
805
            }
806
            for (SpecimenOrObservationBase<?> occurrence : specimensOrObservations) {
807

    
808
//            	direct media removed from specimen #3597
809
//              taxonMedia.addAll(occurrence.getMedia());
810

    
811
                // SpecimenDescriptions
812
                Set<SpecimenDescription> specimenDescriptions = occurrence.getSpecimenDescriptions();
813
                for (DescriptionBase<?> specimenDescription : specimenDescriptions) {
814
                    if (!limitToGalleries || specimenDescription.isImageGallery()) {
815
                        Set<DescriptionElementBase> elements = specimenDescription.getElements();
816
                        for (DescriptionElementBase element : elements) {
817
                            for (Media media : element.getMedia()) {
818
                                taxonMedia.add(media);
819
                            }
820
                        }
821
                    }
822
                }
823

    
824
                if (occurrence.isInstanceOf(DerivedUnit.class)) {
825
                    DerivedUnit derivedUnit = CdmBase.deproxy(occurrence, DerivedUnit.class);
826
                    // Collection
827
                    //TODO why may collections have media attached? #
828
                    if (derivedUnit.getCollection() != null){
829
                        taxonMedia.addAll(derivedUnit.getCollection().getMedia());
830
                    }
831
                }
832
                //media in hierarchy
833
                taxonMedia.addAll(occurrenceService.getMediainHierarchy(occurrence, null, null, propertyPath).getRecords());
834
            }
835
        }
836

    
837
        if(includeTaxonNameDescriptions != null && includeTaxonNameDescriptions) {
838
            logger.trace("listMedia() - includeTaxonNameDescriptions");
839
            // --- TaxonNameDescription
840
            Set<TaxonNameDescription> nameDescriptions = new HashSet<>();
841
            for (Taxon t : taxa) {
842
                nameDescriptions .addAll(t.getName().getDescriptions());
843
            }
844
            for(TaxonNameDescription nameDescription: nameDescriptions){
845
                if (!limitToGalleries || nameDescription.isImageGallery()) {
846
                    Set<DescriptionElementBase> elements = nameDescription.getElements();
847
                    for (DescriptionElementBase element : elements) {
848
                        for (Media media : element.getMedia()) {
849
                            taxonMedia.add(media);
850
                        }
851
                    }
852
                }
853
            }
854
        }
855

    
856

    
857
        logger.trace("listMedia() - initialize");
858
        beanInitializer.initializeAll(taxonMedia, propertyPath);
859

    
860
        logger.trace("listMedia() - END");
861

    
862
        return taxonMedia;
863
    }
864

    
865
    @Override
866
    public List<TaxonBase> findTaxaByID(Set<Integer> listOfIDs) {
867
        return this.dao.loadList(listOfIDs, null);
868
    }
869

    
870
    @Override
871
    public TaxonBase findTaxonByUuid(UUID uuid, List<String> propertyPaths){
872
        return this.dao.findByUuid(uuid, null ,propertyPaths);
873
    }
874

    
875
    @Override
876
    public long countSynonyms(boolean onlyAttachedToTaxon){
877
        return this.dao.countSynonyms(onlyAttachedToTaxon);
878
    }
879

    
880
    @Override
881
    public List<TaxonName> findIdenticalTaxonNames(List<String> propertyPath) {
882
        return this.dao.findIdenticalTaxonNames(propertyPath);
883
    }
884

    
885
    @Override
886
    @Transactional(readOnly=false)
887
    public DeleteResult deleteTaxon(UUID taxonUUID, TaxonDeletionConfigurator config, UUID classificationUuid)  {
888

    
889
    	if (config == null){
890
            config = new TaxonDeletionConfigurator();
891
        }
892
    	Taxon taxon = (Taxon)dao.load(taxonUUID);
893
    	DeleteResult result = new DeleteResult();
894
    	if (taxon == null){
895
    	    result.setAbort();
896
    	    result.addException(new Exception ("The taxon was already deleted."));
897
    	    return result;
898
    	}
899
    	taxon = HibernateProxyHelper.deproxy(taxon);
900
    	Classification classification = HibernateProxyHelper.deproxy(classificationDao.load(classificationUuid), Classification.class);
901
        result = isDeletable(taxonUUID, config);
902

    
903
        if (result.isOk()){
904
            // --- DeleteSynonymRelations
905
            if (config.isDeleteSynonymRelations()){
906
                boolean removeSynonymNameFromHomotypicalGroup = false;
907
                // use tmp Set to avoid concurrent modification
908
                Set<Synonym> synsToDelete = new HashSet<>();
909
                synsToDelete.addAll(taxon.getSynonyms());
910
                for (Synonym synonym : synsToDelete){
911
                    taxon.removeSynonym(synonym, removeSynonymNameFromHomotypicalGroup);
912

    
913
                    // --- DeleteSynonymsIfPossible
914
                    if (config.isDeleteSynonymsIfPossible()){
915
                        //TODO which value
916
                        boolean newHomotypicGroupIfNeeded = true;
917
                        SynonymDeletionConfigurator synConfig = new SynonymDeletionConfigurator();
918
                        result.includeResult(deleteSynonym(synonym, synConfig));
919
                    }
920
                }
921
            }
922

    
923
            // --- DeleteTaxonRelationships
924
            if (! config.isDeleteTaxonRelationships()){
925
                if (taxon.getTaxonRelations().size() > 0){
926
                    result.setAbort();
927
                    result.addException(new Exception("Taxon can't be deleted as it is related to another taxon. " +
928
                            "Remove taxon from all relations to other taxa prior to deletion."));
929
                }
930
            } else{
931
                TaxonDeletionConfigurator configRelTaxon = new TaxonDeletionConfigurator();
932
                configRelTaxon.setDeleteTaxonNodes(false);
933
                configRelTaxon.setDeleteConceptRelationships(true);
934

    
935
                for (TaxonRelationship taxRel: taxon.getTaxonRelations()){
936
                    if (config.isDeleteMisappliedNamesAndInvalidDesignations()
937
                            && taxRel.getType().isMisappliedNameOrInvalidDesignation()
938
                            && taxon.equals(taxRel.getToTaxon())){
939
                        this.deleteTaxon(taxRel.getFromTaxon().getUuid(), config, classificationUuid);
940
                    } else if (config.isDeleteConceptRelationships() && taxRel.getType().isConceptRelationship()){
941

    
942
                        if (taxon.equals(taxRel.getToTaxon()) && isDeletable(taxRel.getFromTaxon().getUuid(), configRelTaxon).isOk()){
943
                            this.deleteTaxon(taxRel.getFromTaxon().getUuid(), configRelTaxon, classificationUuid);
944
                        }else if (isDeletable(taxRel.getToTaxon().getUuid(), configRelTaxon).isOk()){
945
                            this.deleteTaxon(taxRel.getToTaxon().getUuid(), configRelTaxon, classificationUuid);
946
                        }
947
                    }
948
                    taxon.removeTaxonRelation(taxRel);
949
                }
950
            }
951

    
952
            //    	TaxonDescription
953
            if (config.isDeleteDescriptions()){
954
                Set<TaxonDescription> descriptions = taxon.getDescriptions();
955
                List<TaxonDescription> removeDescriptions = new ArrayList<>();
956
                for (TaxonDescription desc: descriptions){
957
                    //TODO use description delete configurator ?
958
                    //FIXME check if description is ALWAYS deletable
959
                    if (desc.getDescribedSpecimenOrObservation() != null){
960
                        result.setAbort();
961
                        result.addException(new Exception("Taxon can't be deleted as it is used in a TaxonDescription" +
962
                                " which also describes specimens or observations"));
963
                        break;
964
                    }
965
                    removeDescriptions.add(desc);
966

    
967

    
968
                }
969
                if (result.isOk()){
970
                    for (TaxonDescription desc: removeDescriptions){
971
                        taxon.removeDescription(desc);
972
                        descriptionService.delete(desc);
973
                    }
974
                } else {
975
                    return result;
976
                }
977
            }
978

    
979

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

    
1043
         if ((taxon.getTaxonNodes() == null || taxon.getTaxonNodes().size()== 0)  && result.isOk()){
1044
             try{
1045
                 dao.delete(taxon);
1046
                 result.addDeletedObject(taxon);
1047
             }catch(Exception e){
1048
                 result.addException(e);
1049
                 result.setError();
1050
             }
1051
         } else {
1052
             result.setError();
1053
             result.addException(new Exception("The Taxon can't be deleted because it is used in a classification."));
1054

    
1055
         }
1056
         //TaxonName
1057
         if (config.isDeleteNameIfPossible() && result.isOk()){
1058
            DeleteResult nameResult = new DeleteResult();
1059
            //remove name if possible (and required)
1060
            if (name != null ){
1061
                nameResult = nameService.delete(name.getUuid(), config.getNameDeletionConfig());
1062
            }
1063
            if (nameResult.isError() || nameResult.isAbort()){
1064
                result.addRelatedObject(name);
1065
                result.addExceptions(nameResult.getExceptions());
1066
            }else{
1067
                result.includeResult(nameResult);
1068
            }
1069
         }
1070
       }
1071

    
1072
       return result;
1073

    
1074
    }
1075

    
1076
    @Override
1077
    @Transactional(readOnly = false)
1078
    public DeleteResult delete(UUID synUUID){
1079
    	Synonym syn = (Synonym)dao.load(synUUID);
1080
        return this.deleteSynonym(syn, null);
1081
    }
1082

    
1083
    @Override
1084
    @Transactional(readOnly = false)
1085
    public DeleteResult deleteSynonym(UUID synonymUuid, SynonymDeletionConfigurator config) {
1086
        return deleteSynonym((Synonym)dao.load(synonymUuid), config);
1087

    
1088
    }
1089

    
1090

    
1091
    @Override
1092
    @Transactional(readOnly = false)
1093
    public DeleteResult deleteSynonym(Synonym synonym, SynonymDeletionConfigurator config) {
1094
        DeleteResult result = new DeleteResult();
1095
    	if (synonym == null){
1096
    		result.setAbort();
1097
    		result.addException(new Exception("The synonym was already deleted."));
1098
    		return result;
1099
        }
1100

    
1101
        if (config == null){
1102
            config = new SynonymDeletionConfigurator();
1103
        }
1104

    
1105
        result = isDeletable(synonym.getUuid(), config);
1106

    
1107
        if (result.isOk()){
1108

    
1109
            synonym = HibernateProxyHelper.deproxy(this.load(synonym.getUuid()), Synonym.class);
1110

    
1111
            //remove synonym
1112
            Taxon accTaxon = synonym.getAcceptedTaxon();
1113

    
1114
            if (accTaxon != null){
1115
                accTaxon = HibernateProxyHelper.deproxy(accTaxon, Taxon.class);
1116
                accTaxon.removeSynonym(synonym, false);
1117
                this.saveOrUpdate(accTaxon);
1118
                result.addUpdatedObject(accTaxon);
1119
            }
1120
            this.saveOrUpdate(synonym);
1121
            //#6281
1122
            dao.flush();
1123

    
1124
            TaxonName name = synonym.getName();
1125
            synonym.setName(null);
1126

    
1127
            dao.delete(synonym);
1128
            result.addDeletedObject(synonym);
1129

    
1130
            //remove name if possible (and required)
1131
            if (name != null && config.isDeleteNameIfPossible()){
1132

    
1133
                    DeleteResult nameDeleteResult = nameService.delete(name, config.getNameDeletionConfig());
1134
                    if (nameDeleteResult.isAbort() || nameDeleteResult.isError()){
1135
                    	result.addExceptions(nameDeleteResult.getExceptions());
1136
                    	result.addRelatedObject(name);
1137
                    }else{
1138
                        result.addDeletedObject(name);
1139
                    }
1140
            }
1141

    
1142
        }
1143
        return result;
1144
    }
1145

    
1146
    @Override
1147
    public List<TaxonName> findIdenticalTaxonNameIds(List<String> propertyPath) {
1148

    
1149
        return this.dao.findIdenticalNamesNew(propertyPath);
1150
    }
1151

    
1152

    
1153
    @Override
1154
    public Taxon findBestMatchingTaxon(String taxonName) {
1155
        MatchingTaxonConfigurator config = MatchingTaxonConfigurator.NewInstance();
1156
        config.setTaxonNameTitle(taxonName);
1157
        return findBestMatchingTaxon(config);
1158
    }
1159

    
1160
    @Override
1161
    public Taxon findBestMatchingTaxon(MatchingTaxonConfigurator config) {
1162

    
1163
        Taxon bestCandidate = null;
1164
        try{
1165
            // 1. search for accepted taxa
1166
            List<TaxonBase> taxonList = dao.findByNameTitleCache(true, false, config.isIncludeUnpublished(),
1167
                    config.getTaxonNameTitle(), null, null, MatchMode.EXACT, null, null, 0, null, null);
1168
            boolean bestCandidateMatchesSecUuid = false;
1169
            boolean bestCandidateIsInClassification = false;
1170
            int countEqualCandidates = 0;
1171
            for(TaxonBase<?> taxonBaseCandidate : taxonList){
1172
                if(taxonBaseCandidate instanceof Taxon){
1173
                    Taxon newCanditate = CdmBase.deproxy(taxonBaseCandidate, Taxon.class);
1174
                    boolean newCandidateMatchesSecUuid = isMatchesSecUuid(newCanditate, config);
1175
                    if (! newCandidateMatchesSecUuid && config.isOnlyMatchingSecUuid() ){
1176
                        continue;
1177
                    }else if(newCandidateMatchesSecUuid && ! bestCandidateMatchesSecUuid){
1178
                        bestCandidate = newCanditate;
1179
                        countEqualCandidates = 1;
1180
                        bestCandidateMatchesSecUuid = true;
1181
                        continue;
1182
                    }
1183

    
1184
                    boolean newCandidateInClassification = isInClassification(newCanditate, config);
1185
                    if (! newCandidateInClassification && config.isOnlyMatchingClassificationUuid()){
1186
                        continue;
1187
                    }else if (newCandidateInClassification && ! bestCandidateIsInClassification){
1188
                        bestCandidate = newCanditate;
1189
                        countEqualCandidates = 1;
1190
                        bestCandidateIsInClassification = true;
1191
                        continue;
1192
                    }
1193
                    if (bestCandidate == null){
1194
                        bestCandidate = newCanditate;
1195
                        countEqualCandidates = 1;
1196
                        continue;
1197
                    }
1198

    
1199
                }else{  //not Taxon.class
1200
                    continue;
1201
                }
1202
                countEqualCandidates++;
1203

    
1204
            }
1205
            if (bestCandidate != null){
1206
                if(countEqualCandidates > 1){
1207
                    logger.info(countEqualCandidates + " equally matching TaxonBases found, using first accepted Taxon: " + bestCandidate.getTitleCache());
1208
                    return bestCandidate;
1209
                } else {
1210
                    logger.info("using accepted Taxon: " + bestCandidate.getTitleCache());
1211
                    return bestCandidate;
1212
                }
1213
            }
1214

    
1215

    
1216
            // 2. search for synonyms
1217
            if (config.isIncludeSynonyms()){
1218
                List<TaxonBase> synonymList = dao.findByNameTitleCache(false, true, config.isIncludeUnpublished(),
1219
                        config.getTaxonNameTitle(), null, null, MatchMode.EXACT, null, null, 0, null, null);
1220
                for(TaxonBase taxonBase : synonymList){
1221
                    if(taxonBase instanceof Synonym){
1222
                        Synonym synonym = CdmBase.deproxy(taxonBase, Synonym.class);
1223
                        bestCandidate = synonym.getAcceptedTaxon();
1224
                        if(bestCandidate != null){
1225
                            logger.info("using accepted Taxon " +  bestCandidate.getTitleCache() + " for synonym " + taxonBase.getTitleCache());
1226
                            return bestCandidate;
1227
                        }
1228
                        //TODO extend method: search using treeUUID, using SecUUID, first find accepted then include synonyms until a matching taxon is found
1229
                    }
1230
                }
1231
            }
1232

    
1233
        } catch (Exception e){
1234
            logger.error(e);
1235
            e.printStackTrace();
1236
        }
1237

    
1238
        return bestCandidate;
1239
    }
1240

    
1241
    private boolean isInClassification(Taxon taxon, MatchingTaxonConfigurator config) {
1242
        UUID configClassificationUuid = config.getClassificationUuid();
1243
        if (configClassificationUuid == null){
1244
            return false;
1245
        }
1246
        for (TaxonNode node : taxon.getTaxonNodes()){
1247
            UUID classUuid = node.getClassification().getUuid();
1248
            if (configClassificationUuid.equals(classUuid)){
1249
                return true;
1250
            }
1251
        }
1252
        return false;
1253
    }
1254

    
1255
    private boolean isMatchesSecUuid(Taxon taxon, MatchingTaxonConfigurator config) {
1256
        UUID configSecUuid = config.getSecUuid();
1257
        if (configSecUuid == null){
1258
            return false;
1259
        }
1260
        UUID taxonSecUuid = (taxon.getSec() == null)? null : taxon.getSec().getUuid();
1261
        return configSecUuid.equals(taxonSecUuid);
1262
    }
1263

    
1264
    @Override
1265
    public Synonym findBestMatchingSynonym(String taxonName, boolean includeUnpublished) {
1266
        List<TaxonBase> synonymList = dao.findByNameTitleCache(false, true, includeUnpublished, taxonName, null, null, MatchMode.EXACT, null, null, 0, null, null);
1267
        if(! synonymList.isEmpty()){
1268
            Synonym result = CdmBase.deproxy(synonymList.iterator().next(), Synonym.class);
1269
            if(synonymList.size() == 1){
1270
                logger.info(synonymList.size() + " Synonym found " + result.getTitleCache() );
1271
                return result;
1272
            } else {
1273
                logger.info("Several matching synonyms found. Using first: " +  result.getTitleCache());
1274
                return result;
1275
            }
1276
        }
1277
        return null;
1278
    }
1279

    
1280
    @Override
1281
    @Transactional(readOnly = false)
1282
    public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym,
1283
            Taxon newTaxon,
1284
            boolean moveHomotypicGroup,
1285
            SynonymType newSynonymType) throws HomotypicalGroupChangeException {
1286
        return moveSynonymToAnotherTaxon(oldSynonym, newTaxon, moveHomotypicGroup,
1287
                newSynonymType,
1288
                oldSynonym.getSec(),
1289
                oldSynonym.getSecMicroReference(),
1290
                true);
1291
    }
1292

    
1293
    @Override
1294
    @Transactional(readOnly = false)
1295
    public UpdateResult moveSynonymToAnotherTaxon(Synonym oldSynonym,
1296
            Taxon newTaxon,
1297
            boolean moveHomotypicGroup,
1298
            SynonymType newSynonymType,
1299
            Reference newSecundum,
1300
            String newSecundumDetail,
1301
            boolean keepSecundumIfUndefined) throws HomotypicalGroupChangeException {
1302

    
1303
        Synonym synonym = CdmBase.deproxy(dao.load(oldSynonym.getUuid()), Synonym.class);
1304
        Taxon oldTaxon = CdmBase.deproxy(dao.load(synonym.getAcceptedTaxon().getUuid()), Taxon.class);
1305
        //TODO what if there is no name ?? Concepts may be cached (e.g. via TCS import)
1306
        TaxonName synonymName = synonym.getName();
1307
        TaxonName fromTaxonName = oldTaxon.getName();
1308
        //set default relationship type
1309
        if (newSynonymType == null){
1310
            newSynonymType = SynonymType.HETEROTYPIC_SYNONYM_OF();
1311
        }
1312
        boolean newRelTypeIsHomotypic = newSynonymType.equals(SynonymType.HOMOTYPIC_SYNONYM_OF());
1313

    
1314
        HomotypicalGroup homotypicGroup = synonymName.getHomotypicalGroup();
1315
        int hgSize = homotypicGroup.getTypifiedNames().size();
1316
        boolean isSingleInGroup = !(hgSize > 1);
1317

    
1318
        if (! isSingleInGroup){
1319
            boolean isHomotypicToAccepted = synonymName.isHomotypic(fromTaxonName);
1320
            boolean hasHomotypicSynonymRelatives = isHomotypicToAccepted ? hgSize > 2 : hgSize > 1;
1321
            if (isHomotypicToAccepted){
1322
                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.";
1323
                String homotypicRelatives = hasHomotypicSynonymRelatives ? " and other synonym(s)":"";
1324
                message = String.format(message, homotypicRelatives);
1325
                throw new HomotypicalGroupChangeException(message);
1326
            }
1327
            if (! moveHomotypicGroup){
1328
                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.";
1329
                throw new HomotypicalGroupChangeException(message);
1330
            }
1331
        }else{
1332
            moveHomotypicGroup = true;  //single synonym always allows to moveCompleteGroup
1333
        }
1334
//        Assert.assertTrue("Synonym can only be moved with complete homotypic group", moveHomotypicGroup);
1335

    
1336
        UpdateResult result = new UpdateResult();
1337
        //move all synonyms to new taxon
1338
        List<Synonym> homotypicSynonyms = oldTaxon.getSynonymsInGroup(homotypicGroup);
1339
        for (Synonym synRelation: homotypicSynonyms){
1340

    
1341
            newTaxon = HibernateProxyHelper.deproxy(newTaxon, Taxon.class);
1342
            oldTaxon = HibernateProxyHelper.deproxy(oldTaxon, Taxon.class);
1343
            oldTaxon.removeSynonym(synRelation, false);
1344
            newTaxon.addSynonym(synRelation, newSynonymType);
1345

    
1346
            if (newSecundum != null || !keepSecundumIfUndefined){
1347
                synRelation.setSec(newSecundum);
1348
            }
1349
            if (newSecundumDetail != null || !keepSecundumIfUndefined){
1350
                synRelation.setSecMicroReference(newSecundumDetail);
1351
            }
1352

    
1353
            //set result  //why is this needed? Seems wrong to me (AM 10.10.2016)
1354
            if (!synRelation.equals(oldSynonym)){
1355
                result.setError();
1356
            }
1357
        }
1358

    
1359
        result.addUpdatedObject(oldTaxon);
1360
        result.addUpdatedObject(newTaxon);
1361
        saveOrUpdate(oldTaxon);
1362
        saveOrUpdate(newTaxon);
1363

    
1364
        return result;
1365
    }
1366

    
1367
    @Override
1368
    public <T extends TaxonBase> List<UuidAndTitleCache<T>> getUuidAndTitleCache(Class<T> clazz, Integer limit, String pattern) {
1369
        return dao.getUuidAndTitleCache(clazz, limit, pattern);
1370
    }
1371

    
1372
    @Override
1373
    public Pager<SearchResult<TaxonBase>> findByFullText(
1374
            Class<? extends TaxonBase> clazz, String queryString,
1375
            Classification classification, TaxonNode subtree, boolean includeUnpublished, List<Language> languages,
1376
            boolean highlightFragments, Integer pageSize, Integer pageNumber,
1377
            List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
1378

    
1379
        LuceneSearch luceneSearch = prepareFindByFullTextSearch(clazz, queryString, classification, subtree,
1380
                null, includeUnpublished, languages, highlightFragments, null);
1381

    
1382
        // --- execute search
1383
        TopGroups<BytesRef> topDocsResultSet;
1384
        try {
1385
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
1386
        } catch (ParseException e) {
1387
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
1388
            luceneParseException.setStackTrace(e.getStackTrace());
1389
            throw luceneParseException;
1390
        }
1391

    
1392
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1393
        idFieldMap.put(CdmBaseType.TAXON, "id");
1394

    
1395
        // ---  initialize taxa, thighlight matches ....
1396
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1397
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1398
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1399

    
1400
        long totalHits = topDocsResultSet != null ? Long.valueOf(topDocsResultSet.totalGroupCount) : 0;
1401
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
1402
    }
1403

    
1404
    @Override
1405
    public Pager<SearchResult<TaxonBase>> findByDistribution(List<NamedArea> areaFilter, List<PresenceAbsenceTerm> statusFilter,
1406
            Classification classification, TaxonNode subtree,
1407
            Integer pageSize, Integer pageNumber,
1408
            List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
1409

    
1410
        LuceneSearch luceneSearch = prepareByDistributionSearch(areaFilter, statusFilter, classification, subtree);
1411

    
1412
        // --- execute search
1413
        TopGroups<BytesRef> topDocsResultSet;
1414
        try {
1415
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
1416
        } catch (ParseException e) {
1417
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
1418
            luceneParseException.setStackTrace(e.getStackTrace());
1419
            throw luceneParseException;
1420
        }
1421

    
1422
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1423
        idFieldMap.put(CdmBaseType.TAXON, "id");
1424

    
1425
        // ---  initialize taxa, thighlight matches ....
1426
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
1427
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1428
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1429

    
1430
        long totalHits = topDocsResultSet != null ? Long.valueOf(topDocsResultSet.totalGroupCount) : 0;
1431
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
1432
    }
1433

    
1434
    /**
1435
     * @param clazz
1436
     * @param queryString
1437
     * @param classification
1438
     * @param includeUnpublished
1439
     * @param languages
1440
     * @param highlightFragments
1441
     * @param sortFields TODO
1442
     * @param directorySelectClass
1443
     * @return
1444
     */
1445
    protected LuceneSearch prepareFindByFullTextSearch(Class<? extends CdmBase> clazz, String queryString,
1446
            Classification classification, TaxonNode subtree, String className, boolean includeUnpublished, List<Language> languages,
1447
            boolean highlightFragments, SortField[] sortFields) {
1448

    
1449
        Builder finalQueryBuilder = new Builder();
1450
        Builder textQueryBuilder = new Builder();
1451

    
1452
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1453
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1454

    
1455
        if(sortFields == null){
1456
            sortFields = new SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING, false)};
1457
        }
1458
        luceneSearch.setSortFields(sortFields);
1459

    
1460
        // ---- search criteria
1461
        luceneSearch.setCdmTypRestriction(clazz);
1462

    
1463
        if(!StringUtils.isEmpty(queryString) && !queryString.equals("*") && !queryString.equals("?") ) {
1464
            textQueryBuilder.add(taxonBaseQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
1465
            textQueryBuilder.add(taxonBaseQueryFactory.newDefinedTermQuery("name.rank", queryString, languages), Occur.SHOULD);
1466
        }
1467
        if(className != null){
1468
            textQueryBuilder.add(taxonBaseQueryFactory.newTermQuery("classInfo.name", className, false), Occur.MUST);
1469
        }
1470

    
1471
        BooleanQuery textQuery = textQueryBuilder.build();
1472
        if(textQuery.clauses().size() > 0) {
1473
            finalQueryBuilder.add(textQuery, Occur.MUST);
1474
        }
1475

    
1476
        if(classification != null){
1477
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery(AcceptedTaxonBridge.DOC_KEY_CLASSIFICATION_ID, classification), Occur.MUST);
1478
        }
1479
        if(subtree != null){
1480
            finalQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(AcceptedTaxonBridge.DOC_KEY_TREEINDEX, subtree.treeIndexWc(), true), Occur.MUST);
1481
        }
1482
        if(!includeUnpublished)  {
1483
            String accPublishParam = TaxonBase.ACC_TAXON_BRIDGE_PREFIX + AcceptedTaxonBridge.DOC_KEY_PUBLISH_SUFFIX;
1484
            finalQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery(accPublishParam, true), Occur.MUST);
1485
            finalQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery("publish", true), Occur.MUST);
1486
        }
1487

    
1488
        luceneSearch.setQuery(finalQueryBuilder.build());
1489

    
1490
        if(highlightFragments){
1491
            luceneSearch.setHighlightFields(taxonBaseQueryFactory.getTextFieldNamesAsArray());
1492
        }
1493
        return luceneSearch;
1494
    }
1495

    
1496
    /**
1497
     * Uses org.apache.lucene.search.join.JoinUtil for query time joining, alternatively
1498
     * the BlockJoinQuery could be used. The latter might be more memory save but has the
1499
     * drawback of requiring to do the join an indexing time.
1500
     * see  http://dev.e-taxonomy.eu/trac/wiki/LuceneNotes#JoinsinLucene for more information on this.
1501
     *
1502
     * Joins TaxonRelationShip with Taxon depending on the direction of the given edge:
1503
     * <ul>
1504
     * <li>direct, everted: {@link Direction.relatedTo}: TaxonRelationShip.relatedTo.id --&gt; Taxon.id </li>
1505
     * <li>inverse: {@link Direction.relatedFrom}:  TaxonRelationShip.relatedFrom.id --&gt; Taxon.id </li>
1506
     * <ul>
1507
     * @param queryString
1508
     * @param classification
1509
     * @param languages
1510
     * @param highlightFragments
1511
     * @param sortFields TODO
1512
     *
1513
     * @return
1514
     * @throws IOException
1515
     */
1516
    protected LuceneSearch prepareFindByTaxonRelationFullTextSearch(TaxonRelationshipEdge edge, String queryString,
1517
            Classification classification, TaxonNode subtree, boolean includeUnpublished, List<Language> languages,
1518
            boolean highlightFragments, SortField[] sortFields) throws IOException {
1519

    
1520
        String fromField;
1521
        String queryTermField;
1522
        String toField = "id"; // TaxonBase.uuid
1523
        String publishField;
1524
        String publishFieldInvers;
1525

    
1526
        if(edge.isBidirectional()){
1527
            throw new RuntimeException("Bidirectional joining not supported!");
1528
        }
1529
        if(edge.isEvers()){
1530
            fromField = "relatedFrom.id";
1531
            queryTermField = "relatedFrom.titleCache";
1532
            publishField = "relatedFrom.publish";
1533
            publishFieldInvers = "relatedTo.publish";
1534
        } else if(edge.isInvers()) {
1535
            fromField = "relatedTo.id";
1536
            queryTermField = "relatedTo.titleCache";
1537
            publishField = "relatedTo.publish";
1538
            publishFieldInvers = "relatedFrom.publish";
1539
        } else {
1540
            throw new RuntimeException("Invalid direction: " + edge.getDirections());
1541
        }
1542

    
1543
        Builder finalQueryBuilder = new Builder();
1544

    
1545
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, TaxonBase.class);
1546
        QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1547

    
1548
        Builder joinFromQueryBuilder = new Builder();
1549
        if(!StringUtils.isEmpty(queryString)){
1550
            joinFromQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(queryTermField, queryString), Occur.MUST);
1551
        }
1552
        joinFromQueryBuilder.add(taxonBaseQueryFactory.newEntityIdsQuery("type.id", edge.getRelationshipTypes()), Occur.MUST);
1553
        if(!includeUnpublished){
1554
            joinFromQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery(publishField, true), Occur.MUST);
1555
            joinFromQueryBuilder.add(taxonBaseQueryFactory.newBooleanQuery(publishFieldInvers, true), Occur.MUST);
1556
        }
1557

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

    
1560
        if(sortFields == null){
1561
            sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING,  false)};
1562
        }
1563
        luceneSearch.setSortFields(sortFields);
1564

    
1565
        finalQueryBuilder.add(joinQuery, Occur.MUST);
1566

    
1567
        if(classification != null){
1568
            finalQueryBuilder.add(taxonBaseQueryFactory.newEntityIdQuery(AcceptedTaxonBridge.DOC_KEY_CLASSIFICATION_ID, classification), Occur.MUST);
1569
        }
1570
        if(subtree != null){
1571
            finalQueryBuilder.add(taxonBaseQueryFactory.newTermQuery(AcceptedTaxonBridge.DOC_KEY_TREEINDEX, subtree.treeIndexWc(), true), Occur.MUST);
1572
        }
1573

    
1574
        luceneSearch.setQuery(finalQueryBuilder.build());
1575

    
1576
        if(highlightFragments){
1577
            luceneSearch.setHighlightFields(taxonBaseQueryFactory.getTextFieldNamesAsArray());
1578
        }
1579
        return luceneSearch;
1580
    }
1581

    
1582
    @Override
1583
    public Pager<SearchResult<TaxonBase>> findTaxaAndNamesByFullText(
1584
            EnumSet<TaxaAndNamesSearchMode> searchModes, String queryString,
1585
            Classification classification, TaxonNode subtree,
1586
            Set<NamedArea> namedAreas, Set<PresenceAbsenceTerm> distributionStatus, List<Language> languages,
1587
            boolean highlightFragments, Integer pageSize,
1588
            Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths)
1589
            throws IOException, LuceneParseException, LuceneMultiSearchException {
1590

    
1591
        // FIXME: allow taxonomic ordering
1592
        //  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";
1593
        // this require building a special sort column by a special classBridge
1594
        if(highlightFragments){
1595
            logger.warn("findTaxaAndNamesByFullText() : fragment highlighting is " +
1596
                    "currently not fully supported by this method and thus " +
1597
                    "may not work with common names and misapplied names.");
1598
        }
1599

    
1600
        // convert sets to lists
1601
        List<NamedArea> namedAreaList = null;
1602
        List<PresenceAbsenceTerm> distributionStatusList = null;
1603
        if(namedAreas != null){
1604
            namedAreaList = new ArrayList<>(namedAreas.size());
1605
            namedAreaList.addAll(namedAreas);
1606
        }
1607
        if(distributionStatus != null){
1608
            distributionStatusList = new ArrayList<>(distributionStatus.size());
1609
            distributionStatusList.addAll(distributionStatus);
1610
        }
1611

    
1612
        // set default if parameter is null
1613
        if(searchModes == null){
1614
            searchModes = EnumSet.of(TaxaAndNamesSearchMode.doTaxa);
1615
        }
1616

    
1617
        // set sort order and thus override any sort orders which may have been
1618
        // defined by prepare*Search methods
1619
        if(orderHints == null){
1620
            orderHints = OrderHint.NOMENCLATURAL_SORT_ORDER.asList();
1621
        }
1622
        SortField[] sortFields = new SortField[orderHints.size()];
1623
        int i = 0;
1624
        for(OrderHint oh : orderHints){
1625
            sortFields[i++] = oh.toSortField();
1626
        }
1627
//        SortField[] sortFields = new SortField[]{SortField.FIELD_SCORE, new SortField("id", SortField.STRING, false)};
1628
//        SortField[] sortFields = new SortField[]{new SortField(NomenclaturalSortOrderBrigde.NAME_SORT_FIELD_NAME, SortField.STRING, false)};
1629

    
1630

    
1631
        boolean addDistributionFilter = namedAreas != null && namedAreas.size() > 0;
1632

    
1633
        List<LuceneSearch> luceneSearches = new ArrayList<>();
1634
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
1635

    
1636
        /*
1637
          ======== filtering by distribution , HOWTO ========
1638

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

    
1644

    
1645
          3. how does it work in spatial?
1646
          see
1647
           - http://www.nsshutdown.com/projects/lucene/whitepaper/locallucene_v2.html
1648
           - http://www.infoq.com/articles/LuceneSpatialSupport
1649
           - http://www.mhaller.de/archives/156-Spatial-search-with-Lucene.html
1650
          ------------------------------------------------------------------------
1651

    
1652
          filter strategies:
1653
          A) use a separate distribution filter per index sub-query/search:
1654
           - byTaxonSyonym (query TaxaonBase):
1655
               use a join area filter (Distribution -> TaxonBase)
1656
           - byCommonName (query DescriptionElementBase): use an area filter on
1657
               DescriptionElementBase !!! PROBLEM !!!
1658
               This cannot work since the distributions are different entities than the
1659
               common names and thus these are different lucene documents.
1660
           - byMisaplliedNames (join query TaxonRelationship -> TaxonBase):
1661
               use a join area filter (Distribution -> TaxonBase)
1662

    
1663
          B) use a common distribution filter for all index sub-query/searches:
1664
           - use a common join area filter (Distribution -> TaxonBase)
1665
           - also implement the byCommonName as join query (CommonName -> TaxonBase)
1666
           PROBLEM in this case: we are losing the fragment highlighting for the
1667
           common names, since the returned documents are always TaxonBases
1668
        */
1669

    
1670
        /* The QueryFactory for creating filter queries on Distributions should
1671
         * The query factory used for the common names query cannot be reused
1672
         * for this case, since we want to only record the text fields which are
1673
         * actually used in the primary query
1674
         */
1675
        QueryFactory distributionFilterQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Distribution.class);
1676

    
1677
        Builder multiIndexByAreaFilterBuilder = new Builder();
1678
        boolean includeUnpublished = searchModes.contains(TaxaAndNamesSearchMode.includeUnpublished);
1679

    
1680
        // search for taxa or synonyms
1681
        if(searchModes.contains(TaxaAndNamesSearchMode.doTaxa) || searchModes.contains(TaxaAndNamesSearchMode.doSynonyms) ) {
1682
            @SuppressWarnings("rawtypes")
1683
            Class<? extends TaxonBase> taxonBaseSubclass = TaxonBase.class;
1684
            String className = null;
1685
            if(searchModes.contains(TaxaAndNamesSearchMode.doTaxa) && !searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
1686
                taxonBaseSubclass = Taxon.class;
1687
            } else if (!searchModes.contains(TaxaAndNamesSearchMode.doTaxa) && searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)) {
1688
                className = "eu.etaxonomy.cdm.model.taxon.Synonym";
1689
            }
1690
            luceneSearches.add(prepareFindByFullTextSearch(taxonBaseSubclass,
1691
                    queryString, classification, subtree, className,
1692
                    includeUnpublished, languages, highlightFragments, sortFields));
1693
            idFieldMap.put(CdmBaseType.TAXON, "id");
1694
            /* A) does not work!!!!
1695
            if(addDistributionFilter){
1696
                // in this case we need a filter which uses a join query
1697
                // to get the TaxonBase documents for the DescriptionElementBase documents
1698
                // which are matching the areas in question
1699
                Query taxonAreaJoinQuery = createByDistributionJoinQuery(
1700
                        namedAreaList,
1701
                        distributionStatusList,
1702
                        distributionFilterQueryFactory
1703
                        );
1704
                multiIndexByAreaFilter.add(new QueryWrapperFilter(taxonAreaJoinQuery), Occur.SHOULD);
1705
            }
1706
            */
1707
            if(addDistributionFilter && searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
1708
                // add additional area filter for synonyms
1709
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1710
                String toField = "accTaxon" + AcceptedTaxonBridge.DOC_KEY_ID_SUFFIX; // id in TaxonBase index
1711

    
1712
                //TODO replace by createByDistributionJoinQuery
1713
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
1714
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class, fromField, true, byDistributionQuery, toField, Taxon.class, ScoreMode.None);
1715
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1716

    
1717
            }
1718
        }
1719

    
1720
        // search by CommonTaxonName
1721
        if(searchModes.contains(TaxaAndNamesSearchMode.doTaxaByCommonNames)) {
1722
            // B)
1723
            QueryFactory descriptionElementQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(DescriptionElementBase.class);
1724
            Query byCommonNameJoinQuery = descriptionElementQueryFactory.newJoinQuery(
1725
                    CommonTaxonName.class,
1726
                    "inDescription.taxon.id",
1727
                    true,
1728
                    QueryFactory.addTypeRestriction(
1729
                                createByDescriptionElementFullTextQuery(queryString, classification, subtree, null, languages, descriptionElementQueryFactory)
1730
                                , CommonTaxonName.class
1731
                                ).build(), "id", null, ScoreMode.Max);
1732
            if (logger.isDebugEnabled()){logger.debug("byCommonNameJoinQuery: " + byCommonNameJoinQuery.toString());}
1733
            LuceneSearch byCommonNameSearch = new LuceneSearch(luceneIndexToolProvider,
1734
                    GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
1735
            byCommonNameSearch.setCdmTypRestriction(Taxon.class);
1736
            Builder builder = new BooleanQuery.Builder();
1737
            builder.add(byCommonNameJoinQuery, Occur.MUST);
1738
            if(!includeUnpublished)  {
1739
                QueryFactory taxonBaseQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(TaxonBase.class);
1740
                builder.add(taxonBaseQueryFactory.newBooleanQuery("publish", true), Occur.MUST);
1741
            }
1742
            byCommonNameSearch.setQuery(builder.build());
1743
            byCommonNameSearch.setSortFields(sortFields);
1744

    
1745
            idFieldMap.put(CdmBaseType.TAXON, "id");
1746

    
1747
            luceneSearches.add(byCommonNameSearch);
1748

    
1749
            /* A) does not work!!!!
1750
            luceneSearches.add(
1751
                    prepareByDescriptionElementFullTextSearch(CommonTaxonName.class,
1752
                            queryString, classification, null, languages, highlightFragments)
1753
                        );
1754
            idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
1755
            if(addDistributionFilter){
1756
                // in this case we are able to use DescriptionElementBase documents
1757
                // which are matching the areas in question directly
1758
                BooleanQuery byDistributionQuery = createByDistributionQuery(
1759
                        namedAreaList,
1760
                        distributionStatusList,
1761
                        distributionFilterQueryFactory
1762
                        );
1763
                multiIndexByAreaFilter.add(new QueryWrapperFilter(byDistributionQuery), Occur.SHOULD);
1764
            } */
1765
        }
1766

    
1767

    
1768
        // search by misapplied names
1769
        //TODO merge with pro parte synonym search once #7487 is fixed
1770
        if(searchModes.contains(TaxaAndNamesSearchMode.doMisappliedNames) /*|| searchModes.contains(TaxaAndNamesSearchMode.doSynonyms) */) {
1771
            // NOTE:
1772
            // prepareFindByTaxonRelationFullTextSearch() is making use of JoinUtil.createJoinQuery()
1773
            // which allows doing query time joins
1774
            // finds the misapplied name (Taxon B) which is an misapplication for
1775
            // a related Taxon A.
1776
            //
1777
            Set<TaxonRelationshipType> relTypes = new HashSet<>();
1778
            if (searchModes.contains(TaxaAndNamesSearchMode.doMisappliedNames)){
1779
                relTypes.addAll(TaxonRelationshipType.allMisappliedNameTypes());
1780
            }
1781
//            if (searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)){
1782
//                relTypes.addAll(TaxonRelationshipType.allSynonymTypes());
1783
//            }
1784

    
1785
            luceneSearches.add(prepareFindByTaxonRelationFullTextSearch(
1786
                    new TaxonRelationshipEdge(relTypes, Direction.relatedTo),
1787
                    queryString, classification, subtree, includeUnpublished, languages, highlightFragments, sortFields));
1788
            idFieldMap.put(CdmBaseType.TAXON, "id");
1789

    
1790
            if(addDistributionFilter){
1791
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1792

    
1793
                /*
1794
                 * Here I was facing a weird and nasty bug which took me bugging be really for hours until I found this solution.
1795
                 * Maybe this is a bug in java itself.
1796
                 *
1797
                 * When the string toField is constructed by using the expression TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString()
1798
                 * directly:
1799
                 *
1800
                 *    String toField = "relation." + TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString() +".to.id";
1801
                 *
1802
                 * The byDistributionQuery fails, however when the uuid is first stored in another string variable the query
1803
                 * will execute as expected:
1804
                 *
1805
                 *    String misappliedNameForUuid = TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString();
1806
                 *    String toField = "relation." + misappliedNameForUuid +".to.id";
1807
                 *
1808
                 * Comparing both strings by the String.equals method returns true, so both String are identical.
1809
                 *
1810
                 * The bug occurs when running eu.etaxonomy.cdm.api.service.TaxonServiceSearchTest in eclipse and in maven and seems to to be
1811
                 * 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)
1812
                 * The bug is persistent after a reboot of the development computer.
1813
                 */
1814
//                String misappliedNameForUuid = TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString();
1815
//                String toField = "relation." + misappliedNameForUuid +".to.id";
1816
                String toField = "relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id";
1817
//                System.out.println("relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id".equals("relation." + misappliedNameForUuid +".to.id") ? " > identical" : " > different");
1818
//                System.out.println("relation.1ed87175-59dd-437e-959e-0d71583d8417.to.id".equals("relation." + TaxonRelationshipType.MISAPPLIED_NAME_FOR().getUuid().toString() +".to.id") ? " > identical" : " > different");
1819

    
1820
                //TODO replace by createByDistributionJoinQuery
1821
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
1822
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class,
1823
                        fromField, true, byDistributionQuery, toField, null, ScoreMode.None);
1824

    
1825
//                debug code for bug described above
1826
                //does not compile anymore since changing from lucene 3.6.2 to lucene 4.10+
1827
//                DocIdSet filterMatchSet = filter.getDocIdSet(luceneIndexToolProvider.getIndexReaderFor(Taxon.class));
1828
//                System.err.println(DocIdBitSetPrinter.docsAsString(filterMatchSet, 100));
1829

    
1830
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1831
            }
1832
        }
1833

    
1834

    
1835
        // search by pro parte synonyms
1836
        if(searchModes.contains(TaxaAndNamesSearchMode.doSynonyms)) {
1837
            //TODO merge with misapplied name search once #7487 is fixed
1838
            Set<TaxonRelationshipType> relTypes = new HashSet<>();
1839
            relTypes.addAll(TaxonRelationshipType.allSynonymTypes());
1840

    
1841
            luceneSearches.add(prepareFindByTaxonRelationFullTextSearch(
1842
                    new TaxonRelationshipEdge(relTypes, Direction.relatedTo),
1843
                    queryString, classification, subtree, includeUnpublished, languages, highlightFragments, sortFields));
1844
            idFieldMap.put(CdmBaseType.TAXON, "id");
1845

    
1846
            if(addDistributionFilter){
1847
                String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1848
                String toField = "relation.8a896603-0fa3-44c6-9cd7-df2d8792e577.to.id";
1849
                BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, distributionFilterQueryFactory);
1850
                Query taxonAreaJoinQuery = distributionFilterQueryFactory.newJoinQuery(Distribution.class,
1851
                        fromField, true, byDistributionQuery, toField, null, ScoreMode.None);
1852
                multiIndexByAreaFilterBuilder.add(taxonAreaJoinQuery, Occur.SHOULD);
1853
            }
1854
        }//end pro parte synonyms
1855

    
1856

    
1857

    
1858
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider,
1859
                luceneSearches.toArray(new LuceneSearch[luceneSearches.size()]));
1860

    
1861

    
1862
        if(addDistributionFilter){
1863

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

    
1879
        if (addDistributionFilter){
1880
            multiSearch.setFilter(multiIndexByAreaFilterBuilder.build());
1881
        }
1882

    
1883

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

    
1894
        // --- initialize taxa, highlight matches ....
1895
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
1896

    
1897

    
1898
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
1899
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
1900

    
1901
        long totalHits = (topDocsResultSet != null) ? Long.valueOf(topDocsResultSet.totalGroupCount) : 0;
1902
        return new DefaultPagerImpl<>(pageNumber, totalHits, pageSize, searchResults);
1903
    }
1904

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

    
1920
        String fromField = "inDescription.taxon.id"; // in DescriptionElementBase index
1921
        String toField = "id"; // id in toType usually this is the TaxonBase index
1922

    
1923
        BooleanQuery byDistributionQuery = createByDistributionQuery(namedAreaList, distributionStatusList, queryFactory);
1924

    
1925
        ScoreMode scoreMode = asFilter ?  ScoreMode.None : ScoreMode.Max;
1926

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

    
1929
        return taxonAreaJoinQuery;
1930
    }
1931

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

    
1944
        // status field from Distribution
1945
        if(distributionStatusList != null && distributionStatusList.size() > 0){
1946
            areaQueryBuilder.add(queryFactory.newEntityIdsQuery("status.id", distributionStatusList), Occur.MUST);
1947
        }
1948

    
1949
        BooleanQuery areaQuery = areaQueryBuilder.build();
1950
        logger.debug("createByDistributionQuery() query: " + areaQuery.toString());
1951
        return areaQuery;
1952
    }
1953

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

    
1969
        Builder finalQueryBuilder = new Builder();
1970

    
1971
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, Taxon.class);
1972

    
1973
        // FIXME is this query factory using the wrong type?
1974
        QueryFactory taxonQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(Taxon.class);
1975

    
1976
        SortField[] sortFields = new  SortField[]{SortField.FIELD_SCORE, new SortField("titleCache__sort", SortField.Type.STRING, false)};
1977
        luceneSearch.setSortFields(sortFields);
1978

    
1979

    
1980
        Query byAreaQuery = createByDistributionJoinQuery(namedAreaList, distributionStatusList, taxonQueryFactory, null, false);
1981

    
1982
        finalQueryBuilder.add(byAreaQuery, Occur.MUST);
1983

    
1984
        if(classification != null){
1985
            finalQueryBuilder.add(taxonQueryFactory.newEntityIdQuery(AcceptedTaxonBridge.DOC_KEY_CLASSIFICATION_ID, classification), Occur.MUST);
1986
        }
1987
        if(subtree != null){
1988
            finalQueryBuilder.add(taxonQueryFactory.newTermQuery(AcceptedTaxonBridge.DOC_KEY_TREEINDEX, subtree.treeIndexWc(), true), Occur.MUST);
1989
        }
1990
        BooleanQuery finalQuery = finalQueryBuilder.build();
1991
        logger.info("prepareByAreaSearch() query: " + finalQuery.toString());
1992
        luceneSearch.setQuery(finalQuery);
1993

    
1994
        return luceneSearch;
1995
    }
1996

    
1997
    @Override
1998
    public Pager<SearchResult<TaxonBase>> findByDescriptionElementFullText(
1999
            Class<? extends DescriptionElementBase> clazz, String queryString,
2000
            Classification classification, TaxonNode subtree, List<Feature> features, List<Language> languages,
2001
            boolean highlightFragments, Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException {
2002

    
2003

    
2004
        LuceneSearch luceneSearch = prepareByDescriptionElementFullTextSearch(clazz, queryString, classification, subtree, features, languages, highlightFragments);
2005

    
2006
        // --- execute search
2007
        TopGroups<BytesRef> topDocsResultSet;
2008
        try {
2009
            topDocsResultSet = luceneSearch.executeSearch(pageSize, pageNumber);
2010
        } catch (ParseException e) {
2011
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2012
            luceneParseException.setStackTrace(e.getStackTrace());
2013
            throw luceneParseException;
2014
        }
2015

    
2016
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
2017
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2018

    
2019
        // --- initialize taxa, highlight matches ....
2020
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(luceneSearch, luceneSearch.getQuery());
2021
        @SuppressWarnings("rawtypes")
2022
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2023
                topDocsResultSet, luceneSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2024

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

    
2028
    }
2029

    
2030

    
2031
    @Override
2032
    public Pager<SearchResult<TaxonBase>> findByEverythingFullText(String queryString,
2033
            Classification classification, TaxonNode subtree, boolean includeUnpublished, List<Language> languages, boolean highlightFragments,
2034
            Integer pageSize, Integer pageNumber, List<OrderHint> orderHints, List<String> propertyPaths) throws IOException, LuceneParseException, LuceneMultiSearchException {
2035

    
2036
        LuceneSearch luceneSearchByDescriptionElement = prepareByDescriptionElementFullTextSearch(null, queryString,
2037
                classification, subtree,
2038
                null, languages, highlightFragments);
2039
        LuceneSearch luceneSearchByTaxonBase = prepareFindByFullTextSearch(null, queryString, classification, subtree, null,
2040
                includeUnpublished, languages, highlightFragments, null);
2041

    
2042
        LuceneMultiSearch multiSearch = new LuceneMultiSearch(luceneIndexToolProvider, luceneSearchByDescriptionElement, luceneSearchByTaxonBase);
2043

    
2044
        // --- execute search
2045
        TopGroups<BytesRef> topDocsResultSet;
2046
        try {
2047
            topDocsResultSet = multiSearch.executeSearch(pageSize, pageNumber);
2048
        } catch (ParseException e) {
2049
            LuceneParseException luceneParseException = new LuceneParseException(e.getMessage());
2050
            luceneParseException.setStackTrace(e.getStackTrace());
2051
            throw luceneParseException;
2052
        }
2053

    
2054
        // --- initialize taxa, highlight matches ....
2055
        ISearchResultBuilder searchResultBuilder = new SearchResultBuilder(multiSearch, multiSearch.getQuery());
2056

    
2057
        Map<CdmBaseType, String> idFieldMap = new HashMap<>();
2058
        idFieldMap.put(CdmBaseType.TAXON, "id");
2059
        idFieldMap.put(CdmBaseType.DESCRIPTION_ELEMENT, "inDescription.taxon.id");
2060

    
2061
        List<SearchResult<TaxonBase>> searchResults = searchResultBuilder.createResultSet(
2062
                topDocsResultSet, multiSearch.getHighlightFields(), dao, idFieldMap, propertyPaths);
2063

    
2064
        int totalHits = topDocsResultSet != null ? topDocsResultSet.totalGroupCount : 0;
2065
        return new DefaultPagerImpl<>(pageNumber, Long.valueOf(totalHits), pageSize, searchResults);
2066

    
2067
    }
2068

    
2069

    
2070
    /**
2071
     * @param clazz
2072
     * @param queryString
2073
     * @param classification
2074
     * @param features
2075
     * @param languages
2076
     * @param highlightFragments
2077
     * @param directorySelectClass
2078
     * @return
2079
     */
2080
    protected LuceneSearch prepareByDescriptionElementFullTextSearch(Class<? extends CdmBase> clazz,
2081
            String queryString, Classification classification, TaxonNode subtree, List<Feature> features,
2082
            List<Language> languages, boolean highlightFragments) {
2083

    
2084
        LuceneSearch luceneSearch = new LuceneSearch(luceneIndexToolProvider, GroupByTaxonClassBridge.GROUPBY_TAXON_FIELD, DescriptionElementBase.class);
2085
        QueryFactory descriptionElementQueryFactory = luceneIndexToolProvider.newQueryFactoryFor(DescriptionElementBase.class);
2086

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

    
2089
        BooleanQuery finalQuery = createByDescriptionElementFullTextQuery(queryString, classification, subtree, features,
2090
                languages, descriptionElementQueryFactory);
2091

    
2092
        luceneSearch.setSortFields(sortFields);
2093
        luceneSearch.setCdmTypRestriction(clazz);
2094
        luceneSearch.setQuery(finalQuery);
2095
        if(highlightFragments){
2096
            luceneSearch.setHighlightFields(descriptionElementQueryFactory.getTextFieldNamesAsArray());
2097
        }
2098

    
2099
        return luceneSearch;
2100
    }
2101

    
2102
    /**
2103
     * @param queryString
2104
     * @param classification
2105
     * @param features
2106
     * @param languages
2107
     * @param descriptionElementQueryFactory
2108
     * @return
2109
     */
2110
    private BooleanQuery createByDescriptionElementFullTextQuery(String queryString,
2111
            Classification classification, TaxonNode subtree, List<Feature> features,
2112
            List<Language> languages, QueryFactory descriptionElementQueryFactory) {
2113

    
2114
        Builder finalQueryBuilder = new Builder();
2115
        Builder textQueryBuilder = new Builder();
2116

    
2117
        if(!StringUtils.isEmpty(queryString)){
2118

    
2119
            textQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("titleCache", queryString), Occur.SHOULD);
2120

    
2121
            // common name
2122
            Builder nameQueryBuilder = new Builder();
2123
            if(languages == null || languages.size() == 0){
2124
                nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2125
            } else {
2126
                Builder languageSubQueryBuilder = new Builder();
2127
                for(Language lang : languages){
2128
                    languageSubQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("language.uuid",  lang.getUuid().toString(), false), Occur.SHOULD);
2129
                }
2130
                nameQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("name", queryString), Occur.MUST);
2131
                nameQueryBuilder.add(languageSubQueryBuilder.build(), Occur.MUST);
2132
            }
2133
            textQueryBuilder.add(nameQueryBuilder.build(), Occur.SHOULD);
2134

    
2135

    
2136
            // text field from TextData
2137
            textQueryBuilder.add(descriptionElementQueryFactory.newMultilanguageTextQuery("text", queryString, languages), Occur.SHOULD);
2138

    
2139
            // --- TermBase fields - by representation ----
2140
            // state field from CategoricalData
2141
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.state", queryString, languages), Occur.SHOULD);
2142

    
2143
            // state field from CategoricalData
2144
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("stateData.modifyingText", queryString, languages), Occur.SHOULD);
2145

    
2146
            // area field from Distribution
2147
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("area", queryString, languages), Occur.SHOULD);
2148

    
2149
            // status field from Distribution
2150
            textQueryBuilder.add(descriptionElementQueryFactory.newDefinedTermQuery("status", queryString, languages), Occur.SHOULD);
2151

    
2152
            finalQueryBuilder.add(textQueryBuilder.build(), Occur.MUST);
2153

    
2154
        }
2155
        // --- classification ----
2156

    
2157

    
2158
        if(classification != null){
2159
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityIdQuery("inDescription.taxon.taxonNodes.classification.id", classification), Occur.MUST);
2160
        }
2161
        if(subtree != null){
2162
            finalQueryBuilder.add(descriptionElementQueryFactory.newTermQuery("inDescription.taxon.taxonNodes.treeIndex", subtree.treeIndexWc(), true), Occur.MUST);
2163
        }
2164

    
2165
        // --- IdentifieableEntity fields - by uuid
2166
        if(features != null && features.size() > 0 ){
2167
            finalQueryBuilder.add(descriptionElementQueryFactory.newEntityUuidsQuery("feature.uuid", features), Occur.MUST);
2168
        }
2169

    
2170
        // the description must be associated with a taxon
2171
        finalQueryBuilder.add(descriptionElementQueryFactory.newIsNotNullQuery("inDescription.taxon.id"), Occur.MUST);
2172

    
2173
        BooleanQuery finalQuery = finalQueryBuilder.build();
2174
        logger.info("prepareByDescriptionElementFullTextSearch() query: " + finalQuery.toString());
2175
        return finalQuery;
2176
    }
2177

    
2178
    @Override
2179
    public List<Synonym> createInferredSynonyms(Taxon taxon, Classification classification, SynonymType type, boolean doWithMisappliedNames){
2180

    
2181

    
2182
        List <Synonym> inferredSynonyms = new ArrayList<>();
2183
        List<Synonym> inferredSynonymsToBeRemoved = new ArrayList<>();
2184

    
2185
        Map <UUID, IZoologicalName> zooHashMap = new HashMap<>();
2186
        boolean includeUnpublished = INCLUDE_UNPUBLISHED;
2187

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

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

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

    
2216
                    //create inferred synonyms for species, subspecies
2217
                    if ((parentName.isGenus() || parentName.isSpecies() || parentName.getRank().equals(Rank.SUBGENUS())) ){
2218

    
2219
                        Synonym inferredEpithet = null;
2220
                        Synonym inferredGenus = null;
2221
                        Synonym potentialCombination = null;
2222

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

    
2229
                        List<Synonym> synonyMsOfParent = dao.getSynonyms(parentTaxon, SynonymType.HETEROTYPIC_SYNONYM_OF(), null, null,orderHintsSynonyms,propertyPaths);
2230
                        List<Synonym> synonymsOfTaxon= dao.getSynonyms(taxon, SynonymType.HETEROTYPIC_SYNONYM_OF(),
2231
                                null, null,orderHintsSynonyms,propertyPaths);
2232

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

    
2244
                        if (type.equals(SynonymType.INFERRED_EPITHET_OF())){
2245
                            for (Synonym synonymRelationOfParent:synonyMsOfParent){
2246

    
2247
                                inferredEpithet = createInferredEpithets(taxon,
2248
                                        zooHashMap, taxonName, epithetOfTaxon,
2249
                                        infragenericEpithetOfTaxon,
2250
                                        infraspecificEpithetOfTaxon,
2251
                                        taxonNames, parentName,
2252
                                        synonymRelationOfParent);
2253

    
2254
                                inferredSynonyms.add(inferredEpithet);
2255
                                zooHashMap.put(inferredEpithet.getName().getUuid(), inferredEpithet.getName());
2256
                                taxonNames.add(inferredEpithet.getName().getNameCache());
2257
                            }
2258

    
2259
                            if (doWithMisappliedNames){
2260

    
2261
                                for (TaxonRelationship taxonRelationship: taxonRelListParent){
2262
                                     Taxon misappliedName = taxonRelationship.getFromTaxon();
2263

    
2264
                                     inferredEpithet = createInferredEpithets(taxon,
2265
                                             zooHashMap, taxonName, epithetOfTaxon,
2266
                                             infragenericEpithetOfTaxon,
2267
                                             infraspecificEpithetOfTaxon,
2268
                                             taxonNames, parentName,
2269
                                             misappliedName);
2270

    
2271
                                    inferredSynonyms.add(inferredEpithet);
2272
                                    zooHashMap.put(inferredEpithet.getName().getUuid(), inferredEpithet.getName());
2273
                                    taxonNames.add(inferredEpithet.getName().getNameCache());
2274
                                }
2275
                            }
2276

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

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

    
2290
                                // Remove identified Synonyms from inferredSynonyms
2291
                                for (Synonym synonym : inferredSynonymsToBeRemoved) {
2292
                                    inferredSynonyms.remove(synonym);
2293
                                }
2294
                            }
2295
                        }
2296

    
2297
                    }else if (type.equals(SynonymType.INFERRED_GENUS_OF())){
2298

    
2299
                        for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2300

    
2301
                            inferredGenus = createInferredGenus(taxon,
2302
                                    zooHashMap, taxonName, epithetOfTaxon,
2303
                                    genusOfTaxon, taxonNames, zooParentName, synonymRelationOfTaxon);
2304

    
2305
                            inferredSynonyms.add(inferredGenus);
2306
                            zooHashMap.put(inferredGenus.getName().getUuid(), inferredGenus.getName());
2307
                            taxonNames.add(inferredGenus.getName().getNameCache());
2308
                        }
2309

    
2310
                        if (doWithMisappliedNames){
2311

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

    
2316
                                inferredSynonyms.add(inferredGenus);
2317
                                zooHashMap.put(inferredGenus.getName().getUuid(), inferredGenus.getName());
2318
                                 taxonNames.add(inferredGenus.getName().getNameCache());
2319
                            }
2320
                        }
2321

    
2322

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

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

    
2336
                                // Remove identified Synonyms from inferredSynonyms
2337
                                for (Synonym synonym : inferredSynonymsToBeRemoved) {
2338
                                    inferredSynonyms.remove(synonym);
2339
                                }
2340
                            }
2341
                        }
2342

    
2343
                    }else if (type.equals(SynonymType.POTENTIAL_COMBINATION_OF())){
2344

    
2345
                        Reference sourceReference = null; // TODO: Determination of sourceReference is redundant
2346
                        //for all synonyms of the parent...
2347
                        for (Synonym synonymRelationOfParent:synonyMsOfParent){
2348
                            TaxonName synName;
2349
                            HibernateProxyHelper.deproxy(synonymRelationOfParent);
2350

    
2351
                            synName = synonymRelationOfParent.getName();
2352

    
2353
                            // Set the sourceReference
2354
                            sourceReference = synonymRelationOfParent.getSec();
2355

    
2356
                            // Determine the idInSource
2357
                            String idInSourceParent = getIdInSource(synonymRelationOfParent);
2358

    
2359
                            IZoologicalName parentSynZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2360
                            String synParentGenus = parentSynZooName.getGenusOrUninomial();
2361
                            String synParentInfragenericName = null;
2362
                            String synParentSpecificEpithet = null;
2363

    
2364
                            if (parentSynZooName.isInfraGeneric()){
2365
                                synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2366
                            }
2367
                            if (parentSynZooName.isSpecies()){
2368
                                synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2369
                            }
2370

    
2371
                           /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2372
                                synonymsGenus.put(synGenusName, idInSource);
2373
                            }*/
2374

    
2375
                            //for all synonyms of the taxon
2376

    
2377
                            for (Synonym synonymRelationOfTaxon:synonymsOfTaxon){
2378

    
2379
                                IZoologicalName zooSynName = getZoologicalName(synonymRelationOfTaxon.getName().getUuid(), zooHashMap);
2380
                                potentialCombination = createPotentialCombination(idInSourceParent, parentSynZooName, zooSynName,
2381
                                        synParentGenus,
2382
                                        synParentInfragenericName,
2383
                                        synParentSpecificEpithet, synonymRelationOfTaxon, zooHashMap);
2384

    
2385
                                taxon.addSynonym(potentialCombination, SynonymType.POTENTIAL_COMBINATION_OF());
2386
                                inferredSynonyms.add(potentialCombination);
2387
                                zooHashMap.put(potentialCombination.getName().getUuid(), potentialCombination.getName());
2388
                                 taxonNames.add(potentialCombination.getName().getNameCache());
2389

    
2390
                            }
2391

    
2392
                        }
2393

    
2394
                        if (doWithMisappliedNames){
2395

    
2396
                            for (TaxonRelationship parentRelationship: taxonRelListParent){
2397

    
2398
                                TaxonName misappliedParentName;
2399

    
2400
                                Taxon misappliedParent = parentRelationship.getFromTaxon();
2401
                                misappliedParentName = misappliedParent.getName();
2402

    
2403
                                HibernateProxyHelper.deproxy(misappliedParent);
2404

    
2405
                                // Set the sourceReference
2406
                                sourceReference = misappliedParent.getSec();
2407

    
2408
                                // Determine the idInSource
2409
                                String idInSourceParent = getIdInSource(misappliedParent);
2410

    
2411
                                IZoologicalName parentSynZooName = getZoologicalName(misappliedParentName.getUuid(), zooHashMap);
2412
                                String synParentGenus = parentSynZooName.getGenusOrUninomial();
2413
                                String synParentInfragenericName = null;
2414
                                String synParentSpecificEpithet = null;
2415

    
2416
                                if (parentSynZooName.isInfraGeneric()){
2417
                                    synParentInfragenericName = parentSynZooName.getInfraGenericEpithet();
2418
                                }
2419
                                if (parentSynZooName.isSpecies()){
2420
                                    synParentSpecificEpithet = parentSynZooName.getSpecificEpithet();
2421
                                }
2422

    
2423

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

    
2433

    
2434
                                    taxon.addSynonym(potentialCombination, SynonymType.POTENTIAL_COMBINATION_OF());
2435
                                    inferredSynonyms.add(potentialCombination);
2436
                                    zooHashMap.put(potentialCombination.getName().getUuid(), potentialCombination.getName());
2437
                                     taxonNames.add(potentialCombination.getName().getNameCache());
2438
                                }
2439
                            }
2440
                        }
2441

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

    
2471
        }
2472

    
2473
        return inferredSynonyms;
2474
    }
2475

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

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

    
2493
                sourceReference = taxon.getSec();
2494
            }
2495
        }
2496
        String synTaxonSpecificEpithet = zooSynName.getSpecificEpithet();
2497

    
2498
        String synTaxonInfraSpecificName= null;
2499

    
2500
        if (parentSynZooName.isSpecies()){
2501
            synTaxonInfraSpecificName = zooSynName.getInfraSpecificEpithet();
2502
        }
2503

    
2504
        /*if (epithetName != null && !synonymsEpithet.contains(epithetName)){
2505
            synonymsEpithet.add(epithetName);
2506
        }*/
2507

    
2508
        //create potential combinations...
2509
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(syn.getName().getRank());
2510

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

    
2526

    
2527
        potentialCombination = Synonym.NewInstance(inferredSynName, null);
2528

    
2529
        // Set the sourceReference
2530
        potentialCombination.setSec(sourceReference);
2531

    
2532

    
2533
        // Determine the idInSource
2534
        String idInSourceSyn= getIdInSource(syn);
2535

    
2536
        if (idInSourceParent != null && idInSourceSyn != null) {
2537
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2538
            inferredSynName.addSource(originalSource);
2539
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation, idInSourceSyn + "; " + idInSourceParent, POTENTIAL_COMBINATION_NAMESPACE, sourceReference, null);
2540
            potentialCombination.addSource(originalSource);
2541
        }
2542

    
2543
        return potentialCombination;
2544
    }
2545

    
2546
    private Synonym createInferredGenus(Taxon taxon,
2547
            Map<UUID, IZoologicalName> zooHashMap, IZoologicalName taxonName,
2548
            String epithetOfTaxon, String genusOfTaxon,
2549
            List<String> taxonNames, IZoologicalName zooParentName,
2550
            TaxonBase syn) {
2551

    
2552
        Synonym inferredGenus;
2553
        TaxonName synName;
2554
        IZoologicalName inferredSynName;
2555
        synName =syn.getName();
2556
        HibernateProxyHelper.deproxy(syn);
2557

    
2558
        // Determine the idInSource
2559
        String idInSourceSyn = getIdInSource(syn);
2560
        String idInSourceTaxon = getIdInSource(taxon);
2561
        // Determine the sourceReference
2562
        Reference sourceReference = syn.getSec();
2563

    
2564
        //logger.warn(sourceReference.getTitleCache());
2565

    
2566
        synName = syn.getName();
2567
        IZoologicalName synZooName = getZoologicalName(synName.getUuid(), zooHashMap);
2568
        String synSpeciesEpithetName = synZooName.getSpecificEpithet();
2569
                     /* if (synonymsEpithet != null && !synonymsEpithet.contains(synSpeciesEpithetName)){
2570
            synonymsEpithet.add(synSpeciesEpithetName);
2571
        }*/
2572

    
2573
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2574
        //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...
2575

    
2576

    
2577
        inferredSynName.setGenusOrUninomial(genusOfTaxon);
2578
        if (zooParentName.isInfraGeneric()){
2579
            inferredSynName.setInfraGenericEpithet(zooParentName.getInfraGenericEpithet());
2580
        }
2581

    
2582
        if (taxonName.isSpecies()){
2583
            inferredSynName.setSpecificEpithet(synSpeciesEpithetName);
2584
        }
2585
        if (taxonName.isInfraSpecific()){
2586
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2587
            inferredSynName.setInfraSpecificEpithet(synZooName.getInfraGenericEpithet());
2588
        }
2589

    
2590

    
2591
        inferredGenus = Synonym.NewInstance(inferredSynName, null);
2592

    
2593
        // Set the sourceReference
2594
        inferredGenus.setSec(sourceReference);
2595

    
2596
        // Add the original source
2597
        if (idInSourceSyn != null && idInSourceTaxon != null) {
2598
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2599
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2600
            inferredGenus.addSource(originalSource);
2601

    
2602
            originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2603
                    idInSourceSyn + "; " + idInSourceTaxon, INFERRED_GENUS_NAMESPACE, sourceReference, null);
2604
            inferredSynName.addSource(originalSource);
2605
            originalSource = null;
2606

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

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

    
2619
        taxon.addSynonym(inferredGenus, SynonymType.INFERRED_GENUS_OF());
2620

    
2621
        return inferredGenus;
2622
    }
2623

    
2624
    private Synonym createInferredEpithets(Taxon taxon,
2625
            Map<UUID, IZoologicalName> zooHashMap, IZoologicalName taxonName,
2626
            String epithetOfTaxon, String infragenericEpithetOfTaxon,
2627
            String infraspecificEpithetOfTaxon, List<String> taxonNames,
2628
            TaxonName parentName, TaxonBase<?> syn) {
2629

    
2630
        Synonym inferredEpithet;
2631
        TaxonName synName;
2632
        IZoologicalName inferredSynName;
2633
        HibernateProxyHelper.deproxy(syn);
2634

    
2635
        // Determine the idInSource
2636
        String idInSourceSyn = getIdInSource(syn);
2637
        String idInSourceTaxon =  getIdInSource(taxon);
2638
        // Determine the sourceReference
2639
        Reference sourceReference = syn.getSec();
2640

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

    
2646
        synName = syn.getName();
2647
        IZoologicalName zooSynName = getZoologicalName(synName.getUuid(), zooHashMap);
2648
        String synGenusName = zooSynName.getGenusOrUninomial();
2649
        String synInfraGenericEpithet = null;
2650
        String synSpecificEpithet = null;
2651

    
2652
        if (zooSynName.getInfraGenericEpithet() != null){
2653
            synInfraGenericEpithet = zooSynName.getInfraGenericEpithet();
2654
        }
2655

    
2656
        if (zooSynName.isInfraSpecific()){
2657
            synSpecificEpithet = zooSynName.getSpecificEpithet();
2658
        }
2659

    
2660
                     /* if (synGenusName != null && !synonymsGenus.containsKey(synGenusName)){
2661
            synonymsGenus.put(synGenusName, idInSource);
2662
        }*/
2663

    
2664
        inferredSynName = TaxonNameFactory.NewZoologicalInstance(taxon.getName().getRank());
2665

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

    
2672
        if (parentName.isInfraGeneric()){
2673
            inferredSynName.setInfraGenericEpithet(synInfraGenericEpithet);
2674
        }
2675
        if (taxonName.isSpecies()){
2676
            inferredSynName.setSpecificEpithet(epithetOfTaxon);
2677
        }else if (taxonName.isInfraSpecific()){
2678
            inferredSynName.setSpecificEpithet(synSpecificEpithet);
2679
            inferredSynName.setInfraSpecificEpithet(infraspecificEpithetOfTaxon);
2680
        }
2681

    
2682
        inferredEpithet = Synonym.NewInstance(inferredSynName, null);
2683

    
2684
        // Set the sourceReference
2685
        inferredEpithet.setSec(sourceReference);
2686

    
2687
        /* Add the original source
2688
        if (idInSource != null) {
2689
            IdentifiableSource originalSource = IdentifiableSource.NewInstance(idInSource, "InferredEpithetOf", syn.getSec(), null);
2690

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

    
2700

    
2701
        IdentifiableSource originalSource = IdentifiableSource.NewInstance(OriginalSourceType.Transformation,
2702
                taxonId, INFERRED_EPITHET_NAMESPACE, sourceReference, null);
2703

    
2704
        inferredEpithet.addSource(originalSource);
2705

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

    
2709
        inferredSynName.addSource(originalSource);
2710

    
2711

    
2712

    
2713
        taxon.addSynonym(inferredEpithet, SynonymType.INFERRED_EPITHET_OF());
2714

    
2715
        return inferredEpithet;
2716
    }
2717

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

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

    
2759

    
2760
        return idInSource;
2761
    }
2762

    
2763

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

    
2780
        return citation;
2781
    }
2782

    
2783
    @Override
2784
    public List<Synonym>  createAllInferredSynonyms(Taxon taxon, Classification tree, boolean doWithMisappliedNames){
2785
        List <Synonym> inferredSynonyms = new ArrayList<>();
2786

    
2787
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.INFERRED_EPITHET_OF(), doWithMisappliedNames));
2788
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.INFERRED_GENUS_OF(), doWithMisappliedNames));
2789
        inferredSynonyms.addAll(createInferredSynonyms(taxon, tree, SynonymType.POTENTIAL_COMBINATION_OF(), doWithMisappliedNames));
2790

    
2791
        return inferredSynonyms;
2792
    }
2793

    
2794
    @Override
2795
    public List<Classification> listClassifications(TaxonBase taxonBase, Integer limit, Integer start, List<String> propertyPaths) {
2796

    
2797
        // TODO quickly implemented, create according dao !!!!
2798
        Set<TaxonNode> nodes = new HashSet<>();
2799
        Set<Classification> classifications = new HashSet<>();
2800
        List<Classification> list = new ArrayList<>();
2801

    
2802
        if (taxonBase == null) {
2803
            return list;
2804
        }
2805

    
2806
        taxonBase = load(taxonBase.getUuid());
2807

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

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

    
2834
        result.addUpdatedObject(fromTaxon);
2835
        result.addUpdatedObject(toTaxon);
2836
        result.addUpdatedObject(result.getCdmEntity());
2837

    
2838
        return result;
2839
    }
2840

    
2841
    @Override
2842
    @Transactional(readOnly = false)
2843
    public UpdateResult changeRelatedTaxonToSynonym(Taxon fromTaxon, Taxon toTaxon, TaxonRelationshipType oldRelationshipType,
2844
            SynonymType synonymType) throws DataChangeNoRollbackException {
2845

    
2846
        UpdateResult result = new UpdateResult();
2847
        // Create new synonym using concept name
2848
        TaxonName synonymName = fromTaxon.getName();
2849

    
2850
        // Remove concept relation from taxon
2851
        toTaxon.removeTaxon(fromTaxon, oldRelationshipType);
2852

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

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

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

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

    
2901
        return result;
2902
    }
2903

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

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

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

    
2936

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

    
2943
                //TaxonInteraction
2944
                if (ref.isInstanceOf(TaxonInteraction.class)){
2945
                    message = "Taxon can't be deleted as it is used in taxonInteraction#taxon2";
2946
                }
2947

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

    
2960
        return result;
2961
    }
2962

    
2963
    @Override
2964
    public IncludedTaxaDTO listIncludedTaxa(UUID taxonUuid, IncludedTaxonConfiguration config) {
2965
        IncludedTaxaDTO result = new IncludedTaxaDTO(taxonUuid);
2966

    
2967
        //preliminary implementation
2968

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

    
2985
        Set<Taxon> related = makeRelatedIncluded(taxa, result, config);
2986
        int i = 0;
2987
        while((! related.isEmpty()) && i++ < 100){  //to avoid
2988
             related = makeRelatedIncluded(related, result, config);
2989
        }
2990

    
2991
        return result;
2992
    }
2993

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

    
3005
        //children
3006
        Set<TaxonNode> taxonNodes = new HashSet<>();
3007
        for (Taxon taxon: uncheckedTaxa){
3008
            taxonNodes.addAll(taxon.getTaxonNodes());
3009
        }
3010

    
3011
        Set<Taxon> children = new HashSet<>();
3012
        if (! config.onlyCongruent){
3013
            for (TaxonNode node: taxonNodes){
3014
                List<TaxonNode> childNodes = nodeService.loadChildNodesOfTaxonNode(node, null, true, config.includeUnpublished, null);
3015
                for (TaxonNode child : childNodes){
3016
                    children.add(child.getTaxon());
3017
                }
3018
            }
3019
            children.remove(null);  // just to be on the save side
3020
        }
3021

    
3022
        Iterator<Taxon> it = children.iterator();
3023
        while(it.hasNext()){
3024
            UUID uuid = it.next().getUuid();
3025
            if (existingTaxa.contains(uuid)){
3026
                it.remove();
3027
            }else{
3028
                existingTaxa.addIncludedTaxon(uuid, new ArrayList<>(), false);
3029
            }
3030
        }
3031

    
3032
        //concept relations
3033
        Set<Taxon> uncheckedAndChildren = new HashSet<>(uncheckedTaxa);
3034
        uncheckedAndChildren.addAll(children);
3035

    
3036
        Set<Taxon> relatedTaxa = makeConceptIncludedTaxa(uncheckedAndChildren, existingTaxa, config);
3037

    
3038

    
3039
        Set<Taxon> result = new HashSet<>(relatedTaxa);
3040
        return result;
3041
    }
3042

    
3043
    /**
3044
     * Computes all conceptually congruent or included taxa and adds them to the existingTaxa data structure.
3045
     * @return the set of these computed taxa
3046
     */
3047
    private Set<Taxon> makeConceptIncludedTaxa(Set<Taxon> unchecked, IncludedTaxaDTO existingTaxa, IncludedTaxonConfiguration config) {
3048
        Set<Taxon> result = new HashSet<>();
3049

    
3050
        for (Taxon taxon : unchecked){
3051
            Set<TaxonRelationship> fromRelations = taxon.getRelationsFromThisTaxon();
3052
            Set<TaxonRelationship> toRelations = taxon.getRelationsToThisTaxon();
3053

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

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

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

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

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

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

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

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

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

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

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

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

    
3167
		return result;
3168
	}
3169

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

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

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

    
3197
        }
3198

    
3199
    	return result;
3200
	}
3201

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

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

    
3214
    /**
3215
     * {@inheritDoc}
3216
     */
3217
    @Override
3218
    public TaxonRelationshipsDTO listTaxonRelationships(UUID taxonUuid, Set<TaxonRelationshipType> directTypes,
3219
            Set<TaxonRelationshipType> inversTypes,
3220
            Direction direction, boolean groupMisapplications,
3221
            boolean includeUnpublished,
3222
            Integer pageSize, Integer pageNumber) {
3223
        TaxonBase<?> taxonBase = dao.load(taxonUuid);
3224
        if (taxonBase == null || !taxonBase.isInstanceOf(TaxonBase.class)){
3225
            //TODO handle
3226
            throw new RuntimeException("Taxon for uuid " + taxonUuid + " not found");
3227
        }else{
3228
            Taxon taxon = CdmBase.deproxy(taxonBase, Taxon.class);
3229
            boolean doDirect = (direction == null || direction == Direction.relatedTo);
3230
            boolean doInvers = (direction == null || direction == Direction.relatedFrom);
3231

    
3232
            TaxonRelationshipsDTO dto = new TaxonRelationshipsDTO();
3233

    
3234
            //TODO paging is difficult because misapplication string is an attribute
3235
            //of toplevel dto
3236
//        long numberOfResults = dao.countTaxonRelationships(taxon, type, includeUnpublished, TaxonRelationship.Direction.relatedFrom);
3237
//        List<TaxonRelationshipsDTO> results = new ArrayList<>();
3238
//        if(numberOfResults > 0) { // no point checking again
3239
//            results = dao.getTaxonRelationships(taxon, type, includeUnpublished, pageSize, pageNumber, orderHints, propertyPaths, TaxonRelationship.Direction.relatedFrom);
3240
//        }
3241
//
3242
//        return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);;
3243

    
3244
            //TODO languages
3245
            List<Language> languages = null;
3246
            if (doDirect){
3247
                direction = Direction.relatedTo;
3248
                //TODO order hints, property path
3249
                List<TaxonRelationship> relations = dao.getTaxonRelationships(taxon, directTypes, includeUnpublished, pageSize, pageNumber, null, null, direction.invers());
3250
                for (TaxonRelationship relation : relations){
3251
                    dto.addRelation(relation, direction, languages);
3252
                }
3253
            }
3254
            if (doInvers){
3255
                direction = Direction.relatedFrom;
3256
                //TODO order hints, property path
3257
                List<TaxonRelationship> relations = dao.getTaxonRelationships(taxon, inversTypes, includeUnpublished, pageSize, pageNumber, null, null, direction.invers());
3258
                for (TaxonRelationship relation : relations){
3259
                    dto.addRelation(relation, direction, languages);
3260
                }
3261
            }
3262
            if (groupMisapplications){
3263
                //TODO
3264
                dto.createMisapplicationString();
3265
            }
3266
            return dto;
3267
        }
3268
    }
3269

    
3270
}
(97-97/103)