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