Project

General

Profile

Download (16.7 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.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
}
(5-5/15)