Project

General

Profile

Download (16 KB) Statistics
| Branch: | Tag: | Revision:
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.description.DescriptionBase;
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.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
     * <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/redmine/issues/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/redmine/issues/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,
90
            boolean subAreaPreference) {
91

    
92
        Map<NamedArea, Set<Distribution>> filteredDistributions = new HashMap<>(100); // start with a big map from the beginning!
93

    
94
        // sort Distributions by the area
95
        for(Distribution distribution : distributions){
96
            NamedArea area = distribution.getArea();
97
            if(area == null) {
98
                logger.debug("skipping distribution with NULL area");
99
                continue;
100
            }
101

    
102
            if(!filteredDistributions.containsKey(area)){
103
                filteredDistributions.put(area, new HashSet<>());
104
            }
105
            filteredDistributions.get(area).add(distribution);
106
        }
107

    
108
        // -------------------------------------------------------------------
109
        // 1) skip distributions having an area with markers matching hideMarkedAreas
110
        //    but keep distributions for fallback areas
111
        if(hiddenAreaMarkerTypes != null && !hiddenAreaMarkerTypes.isEmpty()) {
112
            Set<NamedArea> areasHiddenByMarker = new HashSet<>();
113
            for(NamedArea area : filteredDistributions.keySet()) {
114
                if(checkAreaMarkedHidden(hiddenAreaMarkerTypes, area)) {
115
                    boolean showAsFallbackArea = false;
116
                    // if at least one sub area is not hidden by a marker
117
                    // this area is a fall-back area for this sub area
118
                    for(NamedArea subArea : area.getIncludes()) {
119
                        if (!areasHiddenByMarker.contains(subArea) && checkAreaMarkedHidden(hiddenAreaMarkerTypes, subArea)) {
120
                            if(filteredDistributions.containsKey(subArea)) {
121
                                areasHiddenByMarker.add(subArea);
122
                            }
123
                        }
124
                        // if this sub-area is not marked to be hidden
125
                        // the parent area must be visible if there is no
126
                        // data for the sub-area
127
                        boolean subAreaVisible = filteredDistributions.containsKey(subArea)
128
                                && !areasHiddenByMarker.contains(subArea);
129
                        showAsFallbackArea = !subAreaVisible || showAsFallbackArea;
130
                    }
131
                    if (!showAsFallbackArea) {
132
                        // this area does not need to be shown as
133
                        // fall-back for another area so it will be hidden.
134
                        areasHiddenByMarker.add(area);
135
                    }
136
                }
137
            }
138
            for(NamedArea area :areasHiddenByMarker) {
139
                filteredDistributions.remove(area);
140
            }
141
        }
142
        // -------------------------------------------------------------------
143

    
144

    
145
        // -------------------------------------------------------------------
146
        // 2) remove not computed distributions for areas for which computed
147
        //    distributions exists
148
        //
149
        if(preferComputed) {
150
            Map<NamedArea, Set<Distribution>> computedDistributions = new HashMap<>(distributions.size());
151
            Map<NamedArea, Set<Distribution>> otherDistributions = new HashMap<>(distributions.size());
152
            // separate computed and edited Distributions
153
            for (NamedArea area : filteredDistributions.keySet()) {
154
                for (Distribution distribution : filteredDistributions.get(area)) {
155
                    // this is only required for rule 1
156
                    if(isAggregated(distribution)){
157
                        if(!computedDistributions.containsKey(area)){
158
                            computedDistributions.put(area, new HashSet<>());
159
                        }
160
                        computedDistributions.get(area).add(distribution);
161
                    } else {
162
                        if(!otherDistributions.containsKey(area)){
163
                            otherDistributions.put(area, new HashSet<>());
164
                        }
165
                        otherDistributions.get(area).add(distribution);
166
                    }
167
                }
168
            }
169
            for(NamedArea keyComputed : computedDistributions.keySet()){
170
                otherDistributions.remove(keyComputed);
171
            }
172
            // combine computed and non computed Distributions again
173
            filteredDistributions.clear();
174
            for(NamedArea key : computedDistributions.keySet()){
175
                if(!filteredDistributions.containsKey(key)) {
176
                    filteredDistributions.put(key, new HashSet<>());
177
                }
178
                filteredDistributions.get(key).addAll(computedDistributions.get(key));
179
            }
180
            for(NamedArea key : otherDistributions.keySet()){
181
                if(!filteredDistributions.containsKey(key)) {
182
                    filteredDistributions.put(key, new HashSet<>());
183
                }
184
                filteredDistributions.get(key).addAll(otherDistributions.get(key));
185
            }
186
        }
187
        // -------------------------------------------------------------------
188

    
189

    
190
        // -------------------------------------------------------------------
191
        // 3) statusOrderPreference
192
        if (statusOrderPreference) {
193
            Map<NamedArea, Set<Distribution>> tmpMap = new HashMap<>(filteredDistributions.size());
194
            for(NamedArea key : filteredDistributions.keySet()){
195
                tmpMap.put(key, byHighestOrderPresenceAbsenceTerm(filteredDistributions.get(key)));
196
            }
197
            filteredDistributions = tmpMap;
198
        }
199
        // -------------------------------------------------------------------
200

    
201

    
202
        // -------------------------------------------------------------------
203
        // 4) Sub area preference rule
204
        if(subAreaPreference){
205
            Set<NamedArea> removeCandidatesArea = new HashSet<>();
206
            for(NamedArea key : filteredDistributions.keySet()){
207
                if(removeCandidatesArea.contains(key)){
208
                    continue;
209
                }
210
                if(key.getPartOf() != null && filteredDistributions.containsKey(key.getPartOf())){
211
                    removeCandidatesArea.add(key.getPartOf());
212
                }
213
            }
214
            for(NamedArea removeKey : removeCandidatesArea){
215
                filteredDistributions.remove(removeKey);
216
            }
217
         }
218
        // -------------------------------------------------------------------
219

    
220
        return valuesOfAllInnerSets(filteredDistributions.values());
221
    }
222

    
223
    private static boolean isAggregated(Distribution distribution) {
224
        DescriptionBase<?> desc = distribution.getInDescription();
225
        if (desc != null && desc.isAggregatedDistribution()){
226
            return true;
227
        }
228
        return false;
229
    }
230

    
231
    public static boolean checkAreaMarkedHidden(Set<MarkerType> hiddenAreaMarkerTypes, NamedArea area) {
232
        if(hiddenAreaMarkerTypes != null) {
233
            for(MarkerType markerType : hiddenAreaMarkerTypes){
234
                if(area.hasMarker(markerType, true)){
235
                    return true;
236
                }
237
            }
238
        }
239
        return false;
240
    }
241

    
242
    /**
243
     * Orders the given Distribution elements in a hierarchical structure.
244
     * This method will not filter out any of the Distribution elements.
245
     * @param termDao
246
     * @param omitLevels
247
     * @param hiddenAreaMarkerTypes
248
     *      Areas not associated to a Distribution in the {@code distList} are detected as fall back area
249
     *      if they are having a {@link Marker} with one of the specified {@link MarkerType}s. Areas identified as such
250
     *      are omitted from the hierarchy and the sub areas are moving one level up.
251
     *      For more details on fall back areas see <b>Marked area filter</b> of
252
     *      {@link DescriptionUtility#filterDistributions(Collection, Set, boolean, boolean, boolean)}.
253
     * @param distributionOrder
254
     * @param distList
255
     * @return
256
     */
257
    public static DistributionTree orderDistributions(IDefinedTermDao termDao,
258
            Set<NamedAreaLevel> omitLevels,
259
            Collection<Distribution> distributions,
260
            Set<MarkerType> hiddenAreaMarkerTypes,
261
            DistributionOrder distributionOrder) {
262

    
263
        DistributionTree tree = new DistributionTree(termDao);
264

    
265
        if (logger.isDebugEnabled()){logger.debug("order tree ...");}
266
        //order by areas
267
        tree.orderAsTree(distributions, omitLevels, hiddenAreaMarkerTypes);
268
        tree.recursiveSortChildren(distributionOrder); // FIXME respect current locale for sorting
269
        if (logger.isDebugEnabled()){logger.debug("create tree - DONE");}
270
        return tree;
271
    }
272

    
273
    /**
274
     * Implements the Status order preference filter for a given set to Distributions.
275
     * The distributions should all be for the same area.
276
     * The method returns a site of distributions since multiple Distributions
277
     * with the same status are possible. For example if the same status has been
278
     * published in more than one literature references.
279
     *
280
     * @param distributions
281
     *
282
     * @return the set of distributions with the highest status
283
     */
284
    private static Set<Distribution> byHighestOrderPresenceAbsenceTerm(Set<Distribution> distributions){
285

    
286
        Set<Distribution> preferred = new HashSet<>();
287
        PresenceAbsenceTerm highestStatus = null;  //we need to leave generics here as for some reason highestStatus.compareTo later jumps into the wrong class for calling compareTo
288
        int compareResult;
289
        for (Distribution distribution : distributions) {
290
            if(highestStatus == null){
291
                highestStatus = distribution.getStatus();
292
                preferred.add(distribution);
293
            } else {
294
                if(distribution.getStatus() == null){
295
                    continue;
296
                } else {
297
                    compareResult = highestStatus.compareTo(distribution.getStatus());
298
                }
299
                if(compareResult < 0){
300
                    highestStatus = distribution.getStatus();
301
                    preferred.clear();
302
                    preferred.add(distribution);
303
                } else if(compareResult == 0) {
304
                    preferred.add(distribution);
305
                }
306
            }
307
        }
308

    
309
        return preferred;
310
    }
311

    
312
    private static <T extends CdmBase> Set<T> valuesOfAllInnerSets(Collection<Set<T>> collectionOfSets){
313
        Set<T> allValues = new HashSet<T>();
314
        for(Set<T> set : collectionOfSets){
315
            allValues.addAll(set);
316
        }
317
        return allValues;
318
    }
319

    
320
    /**
321
     * Provides a consistent string based key of the given NamedArea , see also
322
     * {@link #areaKey(Distribution)}
323
     *
324
     * @param area
325
     * @return the string representation of the NamedArea.uuid
326
     */
327
//    private static String areaKey(NamedArea area){
328
//        return String.valueOf(area.getUuid());
329
//    }
330

    
331
    /**
332
     * Provides a consistent string based key of the given NamedArea contained
333
     * in the given distribution, see also {@link #areaKey(Distribution)}
334
     *
335
     * @param distribution
336
     * @return the string representation of the NamedArea.uuid or
337
     *         <code>"NULL"</code> in case the Distribution had no NamedArea
338
     */
339
//    private static String areaKey(Distribution distribution){
340
//        StringBuilder keyBuilder = new StringBuilder();
341
//
342
//        if(distribution.getArea() != null){
343
//            keyBuilder.append(distribution.getArea().getUuid());
344
//        } else {
345
//            keyBuilder.append("NULL");
346
//        }
347
//
348
//        return keyBuilder.toString();
349
//    }
350

    
351
}
(4-4/12)