1
|
/**
|
2
|
* Copyright (C) 2020 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
|
package eu.etaxonomy.cdm.api.service.name;
|
10
|
|
11
|
import java.util.ArrayList;
|
12
|
import java.util.Collections;
|
13
|
import java.util.List;
|
14
|
import java.util.Map;
|
15
|
import java.util.UUID;
|
16
|
|
17
|
import org.apache.commons.lang3.StringUtils;
|
18
|
|
19
|
import eu.etaxonomy.cdm.api.service.name.TypeDesignationSet.TypeDesignationSetType;
|
20
|
import eu.etaxonomy.cdm.common.CdmUtils;
|
21
|
import eu.etaxonomy.cdm.common.UTF8;
|
22
|
import eu.etaxonomy.cdm.format.reference.OriginalSourceFormatter;
|
23
|
import eu.etaxonomy.cdm.hibernate.HibernateProxyHelper;
|
24
|
import eu.etaxonomy.cdm.model.common.IdentifiableEntity;
|
25
|
import eu.etaxonomy.cdm.model.common.IdentifiableSource;
|
26
|
import eu.etaxonomy.cdm.model.common.Language;
|
27
|
import eu.etaxonomy.cdm.model.common.VersionableEntity;
|
28
|
import eu.etaxonomy.cdm.model.name.NameTypeDesignation;
|
29
|
import eu.etaxonomy.cdm.model.name.SpecimenTypeDesignation;
|
30
|
import eu.etaxonomy.cdm.model.name.TextualTypeDesignation;
|
31
|
import eu.etaxonomy.cdm.model.name.TypeDesignationBase;
|
32
|
import eu.etaxonomy.cdm.model.name.TypeDesignationStatusBase;
|
33
|
import eu.etaxonomy.cdm.model.occurrence.DerivedUnit;
|
34
|
import eu.etaxonomy.cdm.model.occurrence.MediaSpecimen;
|
35
|
import eu.etaxonomy.cdm.model.reference.OriginalSourceBase;
|
36
|
import eu.etaxonomy.cdm.model.reference.Reference;
|
37
|
import eu.etaxonomy.cdm.ref.TypedEntityReference;
|
38
|
import eu.etaxonomy.cdm.strategy.cache.HTMLTagRules;
|
39
|
import eu.etaxonomy.cdm.strategy.cache.TagEnum;
|
40
|
import eu.etaxonomy.cdm.strategy.cache.TaggedCacheHelper;
|
41
|
import eu.etaxonomy.cdm.strategy.cache.TaggedText;
|
42
|
import eu.etaxonomy.cdm.strategy.cache.TaggedTextBuilder;
|
43
|
import eu.etaxonomy.cdm.strategy.cache.occurrence.DerivedUnitDefaultCacheStrategy;
|
44
|
|
45
|
/**
|
46
|
* @author a.mueller
|
47
|
* @since 24.11.2020
|
48
|
*/
|
49
|
public class TypeDesignationSetFormatter {
|
50
|
|
51
|
private static final String TYPE_STATUS_SEPARATOR = "; ";
|
52
|
private static final String TYPE_SEPARATOR = "; ";
|
53
|
private static final String TYPE_DESIGNATION_SEPARATOR = ", ";
|
54
|
private static final String TYPE_STATUS_PARENTHESIS_LEFT = " (";
|
55
|
private static final String TYPE_STATUS_PARENTHESIS_RIGHT = ")";
|
56
|
private static final String REFERENCE_PARENTHESIS_RIGHT = "]";
|
57
|
private static final String REFERENCE_PARENTHESIS_LEFT = " [";
|
58
|
private static final String REFERENCE_DESIGNATED_BY = " designated by ";
|
59
|
private static final String REFERENCE_FIDE = "fide ";
|
60
|
private static final String SOURCE_SEPARATOR = ", ";
|
61
|
private static final String POST_STATUS_SEPARATOR = ": ";
|
62
|
private static final String POST_NAME_SEPARTOR = UTF8.EN_DASH_SPATIUM.toString();
|
63
|
|
64
|
private boolean withCitation;
|
65
|
private boolean withStartingTypeLabel;
|
66
|
private boolean withNameIfAvailable;
|
67
|
|
68
|
public static String entityLabel(VersionableEntity baseEntity) {
|
69
|
String label = "";
|
70
|
if(baseEntity instanceof IdentifiableEntity<?>){
|
71
|
label = ((IdentifiableEntity<?>)baseEntity).getTitleCache();
|
72
|
}
|
73
|
//TODO
|
74
|
// else {
|
75
|
// label = baseEntity.toString();
|
76
|
// }
|
77
|
return label;
|
78
|
}
|
79
|
|
80
|
public TypeDesignationSetFormatter(boolean withCitation, boolean withStartingTypeLabel,
|
81
|
boolean withNameIfAvailable) {
|
82
|
this.withCitation = withCitation;
|
83
|
this.withStartingTypeLabel = withStartingTypeLabel;
|
84
|
this.withNameIfAvailable = withNameIfAvailable;
|
85
|
}
|
86
|
|
87
|
public String format(TypeDesignationSetManager manager){
|
88
|
return TaggedCacheHelper.createString(toTaggedText(manager));
|
89
|
}
|
90
|
|
91
|
public String format(TypeDesignationSetManager manager, HTMLTagRules htmlTagRules){
|
92
|
return TaggedCacheHelper.createString(toTaggedText(manager), htmlTagRules);
|
93
|
}
|
94
|
|
95
|
public List<TaggedText> toTaggedText(TypeDesignationSetManager manager){
|
96
|
return buildTaggedText(manager);
|
97
|
}
|
98
|
|
99
|
private List<TaggedText> buildTaggedText(TypeDesignationSetManager manager){
|
100
|
boolean withBrackets = true; //still unclear if this should become a parameter or should be always true
|
101
|
|
102
|
TaggedTextBuilder finalBuilder = new TaggedTextBuilder();
|
103
|
|
104
|
if(withNameIfAvailable && manager.getTypifiedNameCache() != null){
|
105
|
finalBuilder.add(TagEnum.name, manager.getTypifiedNameCache(), TypedEntityReference.fromEntity(manager.getTypifiedName(), false));
|
106
|
finalBuilder.addPostSeparator(POST_NAME_SEPARTOR);
|
107
|
}
|
108
|
|
109
|
int typeSetCount = 0;
|
110
|
Map<VersionableEntity,TypeDesignationSet> orderedByTypesByBaseEntity
|
111
|
= manager.getOrderedTypeDesignationSets();
|
112
|
TypeDesignationSetType lastWsType = null;
|
113
|
if (orderedByTypesByBaseEntity != null){
|
114
|
for(VersionableEntity baseEntity : orderedByTypesByBaseEntity.keySet()) {
|
115
|
buildTaggedTextForSingleTypeSet(manager, withBrackets, finalBuilder,
|
116
|
typeSetCount, baseEntity, lastWsType);
|
117
|
lastWsType = orderedByTypesByBaseEntity.get(baseEntity).getWorkingsetType();
|
118
|
typeSetCount++;
|
119
|
}
|
120
|
}
|
121
|
return finalBuilder.getTaggedText();
|
122
|
}
|
123
|
|
124
|
private void buildTaggedTextForSingleTypeSet(TypeDesignationSetManager manager, boolean withBrackets,
|
125
|
TaggedTextBuilder finalBuilder, int typeSetCount, VersionableEntity baseEntity, TypeDesignationSetType lastWsType) {
|
126
|
|
127
|
Map<VersionableEntity,TypeDesignationSet>
|
128
|
orderedByTypesByBaseEntity = manager.getOrderedTypeDesignationSets();
|
129
|
TypeDesignationSet typeDesignationSet = orderedByTypesByBaseEntity.get(baseEntity);
|
130
|
|
131
|
TaggedTextBuilder workingsetBuilder = new TaggedTextBuilder();
|
132
|
if(typeSetCount > 0){
|
133
|
workingsetBuilder.add(TagEnum.separator, TYPE_SEPARATOR);
|
134
|
}else if (withStartingTypeLabel){
|
135
|
//TODO this is not really exact as we may want to handle specimen types and
|
136
|
//name types separately, but this is such a rare case (if at all) and
|
137
|
//increases complexity so it is not yet implemented
|
138
|
boolean isPlural = hasMultipleTypes(orderedByTypesByBaseEntity);
|
139
|
if(typeDesignationSet.getWorkingsetType().isSpecimenType()){
|
140
|
workingsetBuilder.add(TagEnum.label, (isPlural? "Types:": "Type:"));
|
141
|
} else if (typeDesignationSet.getWorkingsetType().isNameType()){
|
142
|
workingsetBuilder.add(TagEnum.label, (isPlural? "Nametypes:": "Nametype:"));
|
143
|
} else {
|
144
|
//do nothing for now
|
145
|
}
|
146
|
}
|
147
|
|
148
|
boolean hasExplicitBaseEntity = hasExplicitBaseEntity(baseEntity, typeDesignationSet);
|
149
|
if(hasExplicitBaseEntity && !entityLabel(baseEntity).isEmpty()){
|
150
|
workingsetBuilder.add(TagEnum.specimenOrObservation, entityLabel(baseEntity), baseEntity);
|
151
|
}
|
152
|
int typeStatusCount = 0;
|
153
|
if (withBrackets && hasExplicitBaseEntity){
|
154
|
workingsetBuilder.add(TagEnum.separator, TYPE_STATUS_PARENTHESIS_LEFT);
|
155
|
}
|
156
|
for(TypeDesignationStatusBase<?> typeStatus : typeDesignationSet.keySet()) {
|
157
|
typeStatusCount = buildTaggedTextForSingleTypeStatus(manager, workingsetBuilder,
|
158
|
typeDesignationSet, typeStatusCount, typeStatus,
|
159
|
lastWsType, typeSetCount);
|
160
|
}
|
161
|
if (withBrackets && hasExplicitBaseEntity){
|
162
|
workingsetBuilder.add(TagEnum.separator, TYPE_STATUS_PARENTHESIS_RIGHT);
|
163
|
}
|
164
|
typeDesignationSet.setRepresentation(workingsetBuilder.toString());
|
165
|
finalBuilder.addAll(workingsetBuilder);
|
166
|
return;
|
167
|
}
|
168
|
|
169
|
/**
|
170
|
* Checks if the baseType is the same as the (only?) type in the type designation workingset.
|
171
|
*/
|
172
|
private boolean hasExplicitBaseEntity(VersionableEntity baseEntity,
|
173
|
TypeDesignationSet typeDesignationSet) {
|
174
|
if (!typeDesignationSet.isSpecimenWorkingSet()){
|
175
|
return false; //name type designations are not handled here
|
176
|
}else{
|
177
|
UUID baseUuid = baseEntity.getUuid();
|
178
|
for (TypeDesignationDTO<?> dto: typeDesignationSet.getTypeDesignations()){
|
179
|
if (!baseUuid.equals(dto.getTypeUuid())){
|
180
|
return true;
|
181
|
}
|
182
|
}
|
183
|
}
|
184
|
return false;
|
185
|
}
|
186
|
|
187
|
private int buildTaggedTextForSingleTypeStatus(TypeDesignationSetManager manager,
|
188
|
TaggedTextBuilder workingsetBuilder, TypeDesignationSet typeDesignationSet,
|
189
|
int typeStatusCount, TypeDesignationStatusBase<?> typeStatus,
|
190
|
TypeDesignationSetType lastWsType, int typeSetCount) {
|
191
|
|
192
|
//starting separator
|
193
|
if(typeStatusCount++ > 0){
|
194
|
workingsetBuilder.add(TagEnum.separator, TYPE_STATUS_SEPARATOR);
|
195
|
}
|
196
|
|
197
|
boolean isPlural = typeDesignationSet.get(typeStatus).size() > 1;
|
198
|
String label = null;
|
199
|
if(typeStatus != TypeDesignationSet.NULL_STATUS){
|
200
|
label = typeStatus.getLabel();
|
201
|
}else if (typeDesignationSet.getWorkingsetType() != lastWsType
|
202
|
&& (workingsetBuilder.size() > 0 && typeSetCount > 0)){
|
203
|
//only for the first name type (coming after a specimen type add the label (extremely rare case, if at all existing)
|
204
|
if (typeDesignationSet.getWorkingsetType().isNameType()) {
|
205
|
label = "nametype";
|
206
|
}else if (typeDesignationSet.getWorkingsetType().isSpecimenType()) {
|
207
|
label = "type";
|
208
|
}
|
209
|
}
|
210
|
if (label != null){
|
211
|
label = (isPlural ? label + "s" : label);
|
212
|
if (workingsetBuilder.size() == 0){
|
213
|
label = StringUtils.capitalize(label);
|
214
|
}
|
215
|
workingsetBuilder.add(TagEnum.label, label);
|
216
|
workingsetBuilder.add(TagEnum.postSeparator, POST_STATUS_SEPARATOR);
|
217
|
}
|
218
|
|
219
|
//designation + sources
|
220
|
int typeDesignationCount = 0;
|
221
|
for(TypeDesignationDTO<?> typeDesignationDTO : createSortedList(typeDesignationSet, typeStatus)) {
|
222
|
TypeDesignationBase<?> typeDes = manager.findTypeDesignation(typeDesignationDTO.getUuid());
|
223
|
|
224
|
typeDesignationCount = buildTaggedTextForSingleType(typeDes, withCitation,
|
225
|
workingsetBuilder, typeDesignationCount);
|
226
|
}
|
227
|
return typeStatusCount;
|
228
|
}
|
229
|
|
230
|
protected static int buildTaggedTextForSingleType(TypeDesignationBase<?> typeDes, boolean withCitation,
|
231
|
TaggedTextBuilder workingsetBuilder, int typeDesignationCount) {
|
232
|
|
233
|
if(typeDesignationCount++ > 0){
|
234
|
workingsetBuilder.add(TagEnum.separator, TYPE_DESIGNATION_SEPARATOR);
|
235
|
}
|
236
|
buildTaggedTextForTypeDesignationBase(typeDes, workingsetBuilder);
|
237
|
if (withCitation){
|
238
|
|
239
|
//lectotype source
|
240
|
OriginalSourceBase lectoSource = typeDes.getDesignationSource();
|
241
|
if (hasLectoSource(typeDes)){
|
242
|
workingsetBuilder.add(TagEnum.separator, REFERENCE_DESIGNATED_BY);
|
243
|
addSource(workingsetBuilder, lectoSource);
|
244
|
}
|
245
|
//general sources
|
246
|
if (!typeDes.getSources().isEmpty()) {
|
247
|
workingsetBuilder.add(TagEnum.separator, REFERENCE_PARENTHESIS_LEFT + REFERENCE_FIDE);
|
248
|
int count = 0;
|
249
|
for (IdentifiableSource source: typeDes.getSources()){
|
250
|
if (count++ > 0){
|
251
|
workingsetBuilder.add(TagEnum.separator, SOURCE_SEPARATOR);
|
252
|
}
|
253
|
addSource(workingsetBuilder, source);
|
254
|
}
|
255
|
workingsetBuilder.add(TagEnum.separator, REFERENCE_PARENTHESIS_RIGHT);
|
256
|
}
|
257
|
}
|
258
|
|
259
|
return typeDesignationCount;
|
260
|
}
|
261
|
|
262
|
/**
|
263
|
* Adds the tags for the given source.
|
264
|
*/
|
265
|
private static void addSource(TaggedTextBuilder workingsetBuilder,
|
266
|
OriginalSourceBase source) {
|
267
|
Reference ref = source.getCitation();
|
268
|
if (ref != null){
|
269
|
String citation = OriginalSourceFormatter.INSTANCE.format(source);
|
270
|
workingsetBuilder.add(TagEnum.reference, citation, TypedEntityReference.fromEntity(ref, false));
|
271
|
}
|
272
|
}
|
273
|
|
274
|
private static boolean hasLectoSource(TypeDesignationBase<?> typeDes) {
|
275
|
return typeDes.getDesignationSource() != null &&
|
276
|
(typeDes.getDesignationSource().getCitation() != null
|
277
|
|| isNotBlank(typeDes.getDesignationSource().getCitationMicroReference())
|
278
|
);
|
279
|
}
|
280
|
|
281
|
private List<TypeDesignationDTO> createSortedList(
|
282
|
TypeDesignationSet typeDesignationSet, TypeDesignationStatusBase<?> typeStatus) {
|
283
|
|
284
|
List<TypeDesignationDTO> typeDesignationDTOs = new ArrayList<>(typeDesignationSet.get(typeStatus));
|
285
|
Collections.sort(typeDesignationDTOs);
|
286
|
return typeDesignationDTOs;
|
287
|
}
|
288
|
|
289
|
/**
|
290
|
* Returns <code>true</code> if the working set has either multiple working sets
|
291
|
* or if it has a single working set but this workingset has multiple type designations.
|
292
|
*/
|
293
|
private boolean hasMultipleTypes(
|
294
|
Map<VersionableEntity,TypeDesignationSet> typeWorkingSets) {
|
295
|
if (typeWorkingSets == null || typeWorkingSets.isEmpty()){
|
296
|
return false;
|
297
|
}else if (typeWorkingSets.keySet().size() > 1) {
|
298
|
return true;
|
299
|
}
|
300
|
TypeDesignationSet singleSet = typeWorkingSets.values().iterator().next();
|
301
|
return singleSet.getTypeDesignations().size() > 1;
|
302
|
}
|
303
|
|
304
|
private static void buildTaggedTextForTypeDesignationBase(TypeDesignationBase<?> td,
|
305
|
TaggedTextBuilder workingsetBuilder) {
|
306
|
TypedEntityReference<?> typeDesignationEntity = TypedEntityReference.fromEntity(td, false);
|
307
|
if(td instanceof NameTypeDesignation){
|
308
|
buildTaggedTextForNameTypeDesignation((NameTypeDesignation)td, workingsetBuilder, typeDesignationEntity);
|
309
|
} else if (td instanceof TextualTypeDesignation){
|
310
|
buildTaggedTextForTextualTypeDesignation((TextualTypeDesignation)td, workingsetBuilder, typeDesignationEntity);
|
311
|
} else if (td instanceof SpecimenTypeDesignation){
|
312
|
buildTaggedTextForSpecimenTypeDesignation((SpecimenTypeDesignation)td, false, workingsetBuilder, typeDesignationEntity);
|
313
|
}else{
|
314
|
throw new RuntimeException("Unknown TypeDesignation type");
|
315
|
}
|
316
|
}
|
317
|
|
318
|
|
319
|
private static void buildTaggedTextForNameTypeDesignation(NameTypeDesignation td, TaggedTextBuilder workingsetBuilder,
|
320
|
TypedEntityReference<?> typeDesignationEntity) {
|
321
|
|
322
|
if (td.getTypeName() != null){
|
323
|
workingsetBuilder.addAll(td.getTypeName().cacheStrategy().getTaggedTitle(td.getTypeName()));
|
324
|
}
|
325
|
|
326
|
String flags = null;
|
327
|
|
328
|
if(td.isNotDesignated()){
|
329
|
flags = "not designated";
|
330
|
}
|
331
|
if(td.isRejectedType()){
|
332
|
flags = CdmUtils.concat(", ", flags, "rejected");
|
333
|
}
|
334
|
if(td.isConservedType()){
|
335
|
flags = CdmUtils.concat(", ", flags, "conserved");
|
336
|
}
|
337
|
if (flags != null){
|
338
|
workingsetBuilder.add(TagEnum.typeDesignation, flags, typeDesignationEntity);
|
339
|
}
|
340
|
}
|
341
|
|
342
|
private static void buildTaggedTextForTextualTypeDesignation(TextualTypeDesignation td,
|
343
|
TaggedTextBuilder workingsetBuilder, TypedEntityReference<?> typeDesignationEntity) {
|
344
|
|
345
|
String result = td.getPreferredText(Language.DEFAULT());
|
346
|
if (td.isVerbatim()){
|
347
|
result = "\"" + result + "\""; //TODO which character to use?
|
348
|
}
|
349
|
workingsetBuilder.add(TagEnum.typeDesignation, result, typeDesignationEntity);
|
350
|
}
|
351
|
|
352
|
private static void buildTaggedTextForSpecimenTypeDesignation(SpecimenTypeDesignation td, boolean useTitleCache,
|
353
|
TaggedTextBuilder workingsetBuilder, TypedEntityReference<?> typeDesignationEntity) {
|
354
|
|
355
|
if(useTitleCache){
|
356
|
String typeDesigTitle = "";
|
357
|
if(td.getTypeSpecimen() != null){
|
358
|
String nameTitleCache = td.getTypeSpecimen().getTitleCache();
|
359
|
//TODO is this needed?
|
360
|
// if(getTypifiedNameCache() != null){
|
361
|
// nameTitleCache = nameTitleCache.replace(getTypifiedNameCache(), "");
|
362
|
// }
|
363
|
typeDesigTitle += nameTitleCache;
|
364
|
}
|
365
|
workingsetBuilder.add(TagEnum.typeDesignation, typeDesigTitle, typeDesignationEntity);
|
366
|
} else {
|
367
|
if (td.getTypeSpecimen() == null){
|
368
|
workingsetBuilder.add(TagEnum.typeDesignation, "", typeDesignationEntity);
|
369
|
}else{
|
370
|
DerivedUnit du = td.getTypeSpecimen();
|
371
|
if(du.isProtectedTitleCache()){
|
372
|
workingsetBuilder.add(TagEnum.typeDesignation, du.getTitleCache(), typeDesignationEntity);
|
373
|
} else {
|
374
|
du = HibernateProxyHelper.deproxy(du);
|
375
|
boolean isMediaSpecimen = du instanceof MediaSpecimen;
|
376
|
String typeSpecimenTitle = (isMediaSpecimen ? "[icon] " : "");
|
377
|
if(isMediaSpecimen
|
378
|
&& HibernateProxyHelper.deproxyOrNull(du.getCollection()) == null //TODO not sure if only checking the collection is enough, but as we also check existence of sources now the case that only an accession number exists is also covered here
|
379
|
&& ((MediaSpecimen)du).getMediaSpecimen() != null
|
380
|
&& !((MediaSpecimen)du).getMediaSpecimen().getSources().isEmpty()
|
381
|
){
|
382
|
// special case of a published image which is not covered by the DerivedUnitFacadeCacheStrategy
|
383
|
workingsetBuilder.add(TagEnum.typeDesignation, "[icon] in", typeDesignationEntity); //TODO how to better use tagged text here, the type designation itself has no real text; we could include the sources but that makes them unusable as sources :-(
|
384
|
MediaSpecimen msp = (MediaSpecimen)du;
|
385
|
int count = 0;
|
386
|
for(IdentifiableSource source : msp.getMediaSpecimen().getSources()){
|
387
|
if (source.getType().isPrimarySource()){
|
388
|
if (count++ > 0){
|
389
|
workingsetBuilder.add(TagEnum.separator, SOURCE_SEPARATOR);
|
390
|
}
|
391
|
addSource(workingsetBuilder, source);
|
392
|
}
|
393
|
}
|
394
|
} else {
|
395
|
DerivedUnitDefaultCacheStrategy cacheStrategy = DerivedUnitDefaultCacheStrategy.NewInstance(true, false, true, " ");
|
396
|
String titleCache = cacheStrategy.getTitleCache(du, true);
|
397
|
// removing parentheses from code + accession number, see https://dev.e-taxonomy.eu/redmine/issues/8365
|
398
|
titleCache = titleCache.replaceAll("[\\(\\)]", "");
|
399
|
typeSpecimenTitle += titleCache;
|
400
|
workingsetBuilder.add(TagEnum.typeDesignation, typeSpecimenTitle, typeDesignationEntity);
|
401
|
}
|
402
|
} //protected titleCache
|
403
|
}//fi specimen == null
|
404
|
}//fi useTitelCache
|
405
|
|
406
|
if(td.isNotDesignated()){
|
407
|
//this should not happen together with a defined specimen, therefore we may handle it in a separate tag
|
408
|
workingsetBuilder.add(TagEnum.typeDesignation, "not designated", typeDesignationEntity);
|
409
|
}
|
410
|
}
|
411
|
|
412
|
private static boolean isNotBlank(String str){
|
413
|
return StringUtils.isNotBlank(str);
|
414
|
}
|
415
|
}
|