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.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
}
(4-4/12)