Merge branch 'release/4.0.0'
[cdmlib.git] / cdmlib-services / src / main / java / eu / etaxonomy / cdm / api / utility / DescriptionUtility.java
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 }