1
|
/**
|
2
|
* Copyright (C) 2013 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.util;
|
10
|
|
11
|
import java.util.Collection;
|
12
|
import java.util.HashSet;
|
13
|
import java.util.Set;
|
14
|
|
15
|
import org.apache.log4j.Logger;
|
16
|
|
17
|
import eu.etaxonomy.cdm.common.SetMap;
|
18
|
import eu.etaxonomy.cdm.model.common.CdmBase;
|
19
|
import eu.etaxonomy.cdm.model.common.Marker;
|
20
|
import eu.etaxonomy.cdm.model.common.MarkerType;
|
21
|
import eu.etaxonomy.cdm.model.description.DescriptionBase;
|
22
|
import eu.etaxonomy.cdm.model.description.DescriptionType;
|
23
|
import eu.etaxonomy.cdm.model.description.Distribution;
|
24
|
import eu.etaxonomy.cdm.model.description.PresenceAbsenceTerm;
|
25
|
import eu.etaxonomy.cdm.model.location.NamedArea;
|
26
|
import eu.etaxonomy.cdm.model.location.NamedAreaLevel;
|
27
|
import eu.etaxonomy.cdm.model.term.DefinedTermBase;
|
28
|
import eu.etaxonomy.cdm.model.term.OrderedTermBase;
|
29
|
import eu.etaxonomy.cdm.persistence.dao.term.IDefinedTermDao;
|
30
|
|
31
|
/**
|
32
|
* @author a.kohlbecker
|
33
|
* @since Apr 18, 2013
|
34
|
*/
|
35
|
public class DescriptionUtility {
|
36
|
|
37
|
private static final Logger logger = Logger.getLogger(DescriptionUtility.class);
|
38
|
|
39
|
|
40
|
/**
|
41
|
* <b>NOTE: To avoid LayzyLoadingExceptions this method must be used in a transactional context.</b>
|
42
|
*
|
43
|
* Filters the given set of {@link Distribution}s for publication purposes
|
44
|
* The following rules are respected during the filtering:
|
45
|
* <ol>
|
46
|
* <li><b>Marked area filter</b>: Skip distributions for areas having a {@code TRUE} {@link Marker}
|
47
|
* with one of the specified {@link MarkerType}s. Existing sub-areas of a marked area must also be marked
|
48
|
* with the same marker type, otherwise the marked area acts as a <b>fallback area</b> for the sub areas.
|
49
|
* An area is a <b>fallback area</b> if it is marked to be hidden and if it has at least one of
|
50
|
* sub area which is not marked to be hidden. The fallback area will be show if there is no {@link Distribution}
|
51
|
* for any of the non hidden sub-areas. For more detailed discussion on fallback areas see
|
52
|
* https://dev.e-taxonomy.eu/redmine/issues/4408</li>
|
53
|
* <li><b>Prefer aggregated rule</b>: if this flag is set to <code>true</code> aggregated
|
54
|
* distributions are preferred over non-aggregated elements.
|
55
|
* (Aggregated description elements are identified by the description having type
|
56
|
* {@link DescriptionType.AGGREGATED_DISTRIBUTION}). This means if an non-aggregated status
|
57
|
* information exists for the same area for which aggregated data is available,
|
58
|
* the aggregated data has to be given preference over other data.
|
59
|
* see parameter <code>preferAggregated</code></li>
|
60
|
* <li><b>Status order preference rule</b>: In case of multiple distribution
|
61
|
* status ({@link PresenceAbsenceTermBase}) for the same area the status
|
62
|
* with the highest order is preferred, see
|
63
|
* {@link OrderedTermBase#compareTo(OrderedTermBase)}. This rule is
|
64
|
* optional, see parameter <code>statusOrderPreference</code></li>
|
65
|
* <li><b>Sub area preference rule</b>: If there is an area with a <i>direct
|
66
|
* sub area</i> and both areas have the same status only the
|
67
|
* information on the sub area should be reported, whereas the super area
|
68
|
* should be ignored. This rule is optional, see parameter
|
69
|
* <code>subAreaPreference</code>. Can be run separately from the other filters.
|
70
|
* This rule affects any distribution,
|
71
|
* that is to computed and edited equally. For more details see
|
72
|
* {@link https://dev.e-taxonomy.eu/redmine/issues/5050})</li>
|
73
|
* </ol>
|
74
|
*
|
75
|
* @param distributions
|
76
|
* the distributions to filter
|
77
|
* @param hiddenAreaMarkerTypes
|
78
|
* distributions where the area has a {@link Marker} with one of the specified {@link MarkerType}s will
|
79
|
* be skipped or acts as fall back area. For more details see <b>Marked area filter</b> above.
|
80
|
* @param preferAggregated
|
81
|
* Computed distributions for the same area will be preferred over edited distributions.
|
82
|
* <b>This parameter should always be set to <code>true</code>.</b>
|
83
|
* @param statusOrderPreference
|
84
|
* enables the <b>Status order preference rule</b> if set to true,
|
85
|
* This rule can be run separately from the other filters.
|
86
|
* @param subAreaPreference
|
87
|
* enables the <b>Sub area preference rule</b> if set to true
|
88
|
* @param ignoreDistributionStatusUndefined
|
89
|
* workaround until #9500 is implemented
|
90
|
* @return the filtered collection of distribution elements.
|
91
|
*/
|
92
|
public static Set<Distribution> filterDistributions(Collection<Distribution> distributions,
|
93
|
Set<MarkerType> hiddenAreaMarkerTypes, boolean preferAggregated, boolean statusOrderPreference,
|
94
|
boolean subAreaPreference, boolean keepFallBackOnlyIfNoSubareaDataExists, boolean ignoreDistributionStatusUndefined) {
|
95
|
|
96
|
SetMap<NamedArea, Distribution> filteredDistributions = new SetMap<>(distributions.size());
|
97
|
|
98
|
// sort Distributions by the area and filter undefinedStatus
|
99
|
for(Distribution distribution : distributions){
|
100
|
NamedArea area = distribution.getArea();
|
101
|
if(area == null) {
|
102
|
logger.debug("skipping distribution with NULL area");
|
103
|
continue;
|
104
|
}
|
105
|
boolean filterUndefined = ignoreDistributionStatusUndefined && distribution.getStatus() != null
|
106
|
&& distribution.getStatus().getUuid().equals(PresenceAbsenceTerm.uuidUndefined);
|
107
|
if (!filterUndefined){
|
108
|
filteredDistributions.putItem(area, distribution);
|
109
|
}
|
110
|
|
111
|
}
|
112
|
|
113
|
// -------------------------------------------------------------------
|
114
|
// 1) skip distributions having an area with markers matching hiddenAreaMarkerTypes
|
115
|
// but keep distributions for fallback areas (areas with hidden marker, but with visible sub-areas)
|
116
|
if( hiddenAreaMarkerTypes != null && !hiddenAreaMarkerTypes.isEmpty()) {
|
117
|
removeHiddenAndKeepFallbackAreas(hiddenAreaMarkerTypes, filteredDistributions, keepFallBackOnlyIfNoSubareaDataExists);
|
118
|
}
|
119
|
|
120
|
// -------------------------------------------------------------------
|
121
|
// 2) remove not computed distributions for areas for which computed
|
122
|
// distributions exists
|
123
|
if(preferAggregated) {
|
124
|
handlePreferAggregated(filteredDistributions);
|
125
|
}
|
126
|
|
127
|
// -------------------------------------------------------------------
|
128
|
// 3) status order preference rule
|
129
|
if (statusOrderPreference) {
|
130
|
SetMap<NamedArea, Distribution> tmpMap = new SetMap<>(filteredDistributions.size());
|
131
|
for(NamedArea key : filteredDistributions.keySet()){
|
132
|
tmpMap.put(key, filterByHighestDistributionStatusForArea(filteredDistributions.get(key)));
|
133
|
}
|
134
|
filteredDistributions = tmpMap;
|
135
|
}
|
136
|
|
137
|
// -------------------------------------------------------------------
|
138
|
// 4) Sub area preference rule
|
139
|
if(subAreaPreference){
|
140
|
handleSubAreaPreferenceRule(filteredDistributions);
|
141
|
}
|
142
|
|
143
|
return valuesOfAllInnerSets(filteredDistributions.values());
|
144
|
}
|
145
|
|
146
|
private static void handleSubAreaPreferenceRule(SetMap<NamedArea, Distribution> filteredDistributions) {
|
147
|
Set<NamedArea> removeCandidatesArea = new HashSet<>();
|
148
|
for(NamedArea key : filteredDistributions.keySet()){
|
149
|
if(removeCandidatesArea.contains(key)){
|
150
|
continue;
|
151
|
}
|
152
|
if(key.getPartOf() != null && filteredDistributions.containsKey(key.getPartOf())){
|
153
|
removeCandidatesArea.add(key.getPartOf());
|
154
|
}
|
155
|
}
|
156
|
for(NamedArea removeKey : removeCandidatesArea){
|
157
|
filteredDistributions.remove(removeKey);
|
158
|
}
|
159
|
}
|
160
|
|
161
|
/**
|
162
|
* Remove hidden areas but keep fallback areas.
|
163
|
*/
|
164
|
private static void removeHiddenAndKeepFallbackAreas(Set<MarkerType> hiddenAreaMarkerTypes,
|
165
|
SetMap<NamedArea, Distribution> filteredDistributions, boolean keepFallBackOnlyIfNoSubareaDataExists) {
|
166
|
|
167
|
Set<NamedArea> areasHiddenByMarker = new HashSet<>();
|
168
|
for(NamedArea area : filteredDistributions.keySet()) {
|
169
|
if(isMarkedHidden(area, hiddenAreaMarkerTypes)) {
|
170
|
// if at least one sub area is not hidden by a marker
|
171
|
// the given area is a fall-back area for this sub area
|
172
|
SetMap<NamedArea, Distribution> distributionsForSubareaCheck = keepFallBackOnlyIfNoSubareaDataExists ? filteredDistributions : null;
|
173
|
boolean isFallBackArea = isRemainingFallBackArea(area, hiddenAreaMarkerTypes, distributionsForSubareaCheck);
|
174
|
if (!isFallBackArea) {
|
175
|
// this area does not need to be shown as
|
176
|
// fall-back for another area so it will be hidden.
|
177
|
areasHiddenByMarker.add(area);
|
178
|
}
|
179
|
}
|
180
|
}
|
181
|
for(NamedArea area :areasHiddenByMarker) {
|
182
|
filteredDistributions.remove(area);
|
183
|
}
|
184
|
}
|
185
|
|
186
|
//if filteredDistributions == null it can be ignored if data exists or not
|
187
|
private static boolean isRemainingFallBackArea(NamedArea area, Set<MarkerType> hiddenAreaMarkerTypes,
|
188
|
SetMap<NamedArea, Distribution> filteredDistributions) {
|
189
|
|
190
|
boolean result = false;
|
191
|
for(DefinedTermBase<NamedArea> included : area.getIncludes()) {
|
192
|
NamedArea subArea = CdmBase.deproxy(included,NamedArea.class);
|
193
|
boolean noOrIgnoreData = filteredDistributions == null || !filteredDistributions.containsKey(subArea);
|
194
|
|
195
|
//if subarea is not hidden and data exists return true
|
196
|
if (isMarkedHidden(subArea, hiddenAreaMarkerTypes)){
|
197
|
boolean subAreaIsFallback = isRemainingFallBackArea(subArea, hiddenAreaMarkerTypes, filteredDistributions);
|
198
|
if (subAreaIsFallback && noOrIgnoreData){
|
199
|
return true;
|
200
|
}else{
|
201
|
continue;
|
202
|
}
|
203
|
}else{ //subarea not marked hidden
|
204
|
if (noOrIgnoreData){
|
205
|
return true;
|
206
|
}else{
|
207
|
continue;
|
208
|
}
|
209
|
}
|
210
|
// boolean isNotHidden_AndHasNoData_OrDataCanBeIgnored =
|
211
|
// && noOrIgnoreData && subArea.getIncludes().isEmpty();
|
212
|
// if (isNotHidden_AndHasNoData_OrDataCanBeIgnored) {
|
213
|
// return true;
|
214
|
// }
|
215
|
// if (!isMarkedHidden(subArea, hiddenAreaMarkerTypes) ){
|
216
|
//
|
217
|
// }
|
218
|
//
|
219
|
// //do the same recursively
|
220
|
// boolean hasVisibleSubSubarea = isRemainingFallBackArea(subArea, hiddenAreaMarkerTypes, filteredDistributions, areasHiddenByMarker);
|
221
|
// if (hasVisibleSubSubarea){
|
222
|
// return true;
|
223
|
// }
|
224
|
}
|
225
|
return false;
|
226
|
}
|
227
|
|
228
|
private static void handlePreferAggregated(SetMap<NamedArea, Distribution> filteredDistributions) {
|
229
|
SetMap<NamedArea, Distribution> computedDistributions = new SetMap<>(filteredDistributions.size());
|
230
|
SetMap<NamedArea, Distribution> nonComputedDistributions = new SetMap<>(filteredDistributions.size());
|
231
|
// separate computed and edited Distributions
|
232
|
for (NamedArea area : filteredDistributions.keySet()) {
|
233
|
for (Distribution distribution : filteredDistributions.get(area)) {
|
234
|
// this is only required for rule 1
|
235
|
if(isAggregated(distribution)){
|
236
|
computedDistributions.putItem(area, distribution);
|
237
|
} else {
|
238
|
nonComputedDistributions.putItem(area,distribution);
|
239
|
}
|
240
|
}
|
241
|
}
|
242
|
//remove nonComputed distributions for which computed distributions exist in the same area
|
243
|
for(NamedArea keyComputed : computedDistributions.keySet()){
|
244
|
nonComputedDistributions.remove(keyComputed);
|
245
|
}
|
246
|
// combine computed and non computed Distributions again
|
247
|
filteredDistributions.clear();
|
248
|
for(NamedArea area : computedDistributions.keySet()){
|
249
|
filteredDistributions.put(area, computedDistributions.get(area)); //is it a problem that we use the same interal Set here?
|
250
|
}
|
251
|
for(NamedArea area : nonComputedDistributions.keySet()){
|
252
|
filteredDistributions.put(area, nonComputedDistributions.get(area));
|
253
|
}
|
254
|
}
|
255
|
|
256
|
private static boolean isAggregated(Distribution distribution) {
|
257
|
DescriptionBase<?> desc = distribution.getInDescription();
|
258
|
if (desc != null && desc.isAggregatedDistribution()){
|
259
|
return true;
|
260
|
}
|
261
|
return false;
|
262
|
}
|
263
|
|
264
|
protected static boolean isMarkedHidden(NamedArea area, Set<MarkerType> hiddenAreaMarkerTypes) {
|
265
|
if(hiddenAreaMarkerTypes != null) {
|
266
|
for(MarkerType markerType : hiddenAreaMarkerTypes){
|
267
|
if(area.hasMarker(markerType, true)){
|
268
|
return true;
|
269
|
}
|
270
|
}
|
271
|
}
|
272
|
return false;
|
273
|
}
|
274
|
|
275
|
/**
|
276
|
* Orders the given Distribution elements in a hierarchical structure.
|
277
|
* This method will not filter out any of the distribution elements.
|
278
|
* @param omitLevels
|
279
|
* @param distributions
|
280
|
* @param fallbackAreaMarkerTypes
|
281
|
* Areas are fallback areas if they have a {@link Marker} with one of the specified
|
282
|
* {@link MarkerType marker types}.
|
283
|
* Areas identified as such are omitted from the hierarchy and the sub areas are moving one level up.
|
284
|
* This may not be the case if the fallback area has a distribution record itself AND if
|
285
|
* neverUseFallbackAreasAsParents is <code>false</code>.
|
286
|
* For more details on fall back areas see <b>Marked area filter</b> of
|
287
|
* {@link DescriptionUtility#filterDistributions(Collection, Set, boolean, boolean, boolean)}.
|
288
|
* @param distributionOrder
|
289
|
* @param termDao
|
290
|
* Currently used from performance reasons (preloading of parent areas), may be removed in future
|
291
|
* @return the {@link DistributionTree distribution tree}
|
292
|
*/
|
293
|
public static DistributionTree buildOrderedTree(Set<NamedAreaLevel> omitLevels,
|
294
|
Collection<Distribution> distributions,
|
295
|
Set<MarkerType> fallbackAreaMarkerTypes,
|
296
|
boolean neverUseFallbackAreaAsParent,
|
297
|
DistributionOrder distributionOrder,
|
298
|
IDefinedTermDao termDao) {
|
299
|
|
300
|
DistributionTree tree = new DistributionTree(termDao);
|
301
|
|
302
|
if (logger.isDebugEnabled()){logger.debug("order tree ...");}
|
303
|
//order by areas
|
304
|
tree.orderAsTree(distributions, omitLevels, fallbackAreaMarkerTypes, neverUseFallbackAreaAsParent);
|
305
|
tree.recursiveSortChildren(distributionOrder); // TODO respect current locale for sorting
|
306
|
if (logger.isDebugEnabled()){logger.debug("create tree - DONE");}
|
307
|
return tree;
|
308
|
}
|
309
|
|
310
|
/**
|
311
|
* Implements the Status order preference filter for a given set to Distributions.
|
312
|
* The distributions should all be for the same area.
|
313
|
* The method returns a site of distributions since multiple Distributions
|
314
|
* with the same status are possible. For example if the same status has been
|
315
|
* published in more than one literature references.
|
316
|
*
|
317
|
* @param distributions
|
318
|
*
|
319
|
* @return the set of distributions with the highest status
|
320
|
*/
|
321
|
private static Set<Distribution> filterByHighestDistributionStatusForArea(Set<Distribution> distributions){
|
322
|
|
323
|
Set<Distribution> preferred = new HashSet<>();
|
324
|
PresenceAbsenceTerm highestStatus = null; //we need to leave generics here as for some reason highestStatus.compareTo later jumps into the wrong class for calling compareTo
|
325
|
int compareResult;
|
326
|
for (Distribution distribution : distributions) {
|
327
|
if(highestStatus == null){
|
328
|
highestStatus = distribution.getStatus();
|
329
|
preferred.add(distribution);
|
330
|
} else {
|
331
|
if(distribution.getStatus() == null){
|
332
|
continue;
|
333
|
} else {
|
334
|
compareResult = highestStatus.compareTo(distribution.getStatus());
|
335
|
}
|
336
|
if(compareResult < 0){
|
337
|
highestStatus = distribution.getStatus();
|
338
|
preferred.clear();
|
339
|
preferred.add(distribution);
|
340
|
} else if(compareResult == 0) {
|
341
|
preferred.add(distribution);
|
342
|
}
|
343
|
}
|
344
|
}
|
345
|
|
346
|
return preferred;
|
347
|
}
|
348
|
|
349
|
private static <T extends CdmBase> Set<T> valuesOfAllInnerSets(Collection<Set<T>> collectionOfSets){
|
350
|
Set<T> allValues = new HashSet<T>();
|
351
|
for(Set<T> set : collectionOfSets){
|
352
|
allValues.addAll(set);
|
353
|
}
|
354
|
return allValues;
|
355
|
}
|
356
|
|
357
|
}
|