Project

General

Profile

Download (16 KB) Statistics
| Branch: | Tag: | Revision:
1
// $Id$
2
/**
3
* Copyright (C) 2013 EDIT
4
* European Distributed Institute of Taxonomy
5
* http://www.e-taxonomy.eu
6
*
7
* The contents of this file are subject to the Mozilla Public License Version 1.1
8
* See LICENSE.TXT at the top of this package for the full license terms.
9
*/
10
package eu.etaxonomy.cdm.api.utility;
11

    
12
import java.util.Collection;
13
import java.util.HashMap;
14
import java.util.HashSet;
15
import java.util.Map;
16
import java.util.Set;
17

    
18
import org.apache.log4j.Logger;
19

    
20
import eu.etaxonomy.cdm.api.service.DistributionTree;
21
import eu.etaxonomy.cdm.model.common.CdmBase;
22
import eu.etaxonomy.cdm.model.common.Marker;
23
import eu.etaxonomy.cdm.model.common.MarkerType;
24
import eu.etaxonomy.cdm.model.common.OrderedTermBase;
25
import eu.etaxonomy.cdm.model.description.Distribution;
26
import eu.etaxonomy.cdm.model.description.PresenceAbsenceTerm;
27
import eu.etaxonomy.cdm.model.location.NamedArea;
28
import eu.etaxonomy.cdm.model.location.NamedAreaLevel;
29
import eu.etaxonomy.cdm.persistence.dao.common.IDefinedTermDao;
30

    
31
/**
32
 * @author a.kohlbecker
33
 * @date Apr 18, 2013
34
 *
35
 */
36
public class DescriptionUtility {
37

    
38
    private static final Logger logger = Logger.getLogger(DescriptionUtility.class);
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/trac/ticket/4408</li>
53
     * <li><b>Prefer computed rule</b>:Computed distributions are preferred over entered or imported elements.
54
     * (Computed description elements are identified by the {@link
55
     * MarkerType.COMPUTED()}). This means if a entered or imported status
56
     * information exist for the same area for which computed data is available,
57
     * the computed data has to be given preference over other data.
58
     * see parameter <code>preferComputed</code></li>
59
     * <li><b>Status order preference rule</b>: In case of multiple distribution
60
     * status ({@link PresenceAbsenceTermBase}) for the same area the status
61
     * with the highest order is preferred, see
62
     * {@link OrderedTermBase#compareTo(OrderedTermBase)}. This rule is
63
     * optional, see parameter <code>statusOrderPreference</code></li>
64
     * <li><b>Sub area preference rule</b>: If there is an area with a <i>direct
65
     * sub area</i> and both areas have the same status only the
66
     * information on the sub area should be reported, whereas the super area
67
     * should be ignored. This rule is optional, see parameter
68
     * <code>subAreaPreference</code>. Can be run separately from the other filters.
69
     * This rule affects any distribution,
70
     * that is to computed and edited equally. For more details see
71
     * {@link https://dev.e-taxonomy.eu/trac/ticket/5050})</li>
72
     * </ol>
73
     *
74
     * @param distributions
75
     *            the distributions to filter
76
     * @param hiddenAreaMarkerTypes
77
     *            distributions where the area has a {@link Marker} with one of the specified {@link MarkerType}s will
78
     *            be skipped or acts as fall back area. For more details see <b>Marked area filter</b> above.
79
     * @param preferComputed
80
     *            Computed distributions for the same area will be preferred over edited distributions.
81
     *            <b>This parameter should always be set to <code>true</code>.</b>
82
     * @param statusOrderPreference
83
     *            enables the <b>Status order preference rule</b> if set to true,
84
     *            This rule can be run separately from the other filters.
85
     * @param subAreaPreference
86
     *            enables the <b>Sub area preference rule</b> if set to true
87
     * @return the filtered collection of distribution elements.
88
     */
89
    public static Set<Distribution> filterDistributions(Collection<Distribution> distributions,
90
            Set<MarkerType> hiddenAreaMarkerTypes, boolean preferComputed, boolean statusOrderPreference, boolean subAreaPreference) {
91

    
92
        Map<NamedArea, Set<Distribution>> filteredDistributions = new HashMap<NamedArea, Set<Distribution>>(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<Distribution>());
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<NamedArea>();
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 subarea is not marked to be hidden
125
                        // the parent area must be visible if there is no
126
                        // data for the subarea
127
                        boolean subAreaVisible = filteredDistributions.containsKey(subArea) &&  !areasHiddenByMarker.contains(subArea);
128
                        showAsFallbackArea = !subAreaVisible || showAsFallbackArea;
129
                    }
130
                    if (!showAsFallbackArea) {
131
                        // this area does not need to be shown as
132
                        // fallback for another area
133
                        // 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<NamedArea, Set<Distribution>>(distributions.size());
151
            Map<NamedArea, Set<Distribution>> otherDistributions = new HashMap<NamedArea, Set<Distribution>>(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(distribution.hasMarker(MarkerType.COMPUTED(), true)){
157
                        if(!computedDistributions.containsKey(area)){
158
                            computedDistributions.put(area, new HashSet<Distribution>());
159
                        }
160
                        computedDistributions.get(area).add(distribution);
161
                    } else {
162
                        if(!otherDistributions.containsKey(area)){
163
                            otherDistributions.put(area, new HashSet<Distribution>());
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<Distribution>());
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<Distribution>());
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<NamedArea, Set<Distribution>>(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<NamedArea>();
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
    /**
224
     * @param hiddenAreaMarkerTypes
225
     * @param area
226
     * @param isMarkedHidden
227
     * @return
228
     */
229
    public static boolean checkAreaMarkedHidden(Set<MarkerType> hiddenAreaMarkerTypes, NamedArea area) {
230
        if(hiddenAreaMarkerTypes != null) {
231
            for(MarkerType markerType : hiddenAreaMarkerTypes){
232
                if(area.hasMarker(markerType, true)){
233
                    return true;
234
                }
235
            }
236
        }
237
        return false;
238
    }
239

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

    
261
        DistributionTree tree = new DistributionTree(termDao);
262

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

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

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

    
307
        return preferred;
308
    }
309

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

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

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

    
349
}
(1-1/4)