Merge branch 'develop' of ssh://dev.e-taxonomy.eu/var/git/cdmlib into develop
[cdmlib.git] / cdmlib-services / src / main / java / eu / etaxonomy / cdm / api / service / DistributionTree.java
1 /**
2 * Copyright (C) 2007 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
10 package eu.etaxonomy.cdm.api.service;
11
12 import java.io.Serializable;
13 import java.util.ArrayList;
14 import java.util.Collection;
15 import java.util.Collections;
16 import java.util.Comparator;
17 import java.util.HashSet;
18 import java.util.List;
19 import java.util.Set;
20
21 import org.apache.log4j.Logger;
22 import org.hibernate.proxy.HibernateProxy;
23
24 import eu.etaxonomy.cdm.api.utility.DescriptionUtility;
25 import eu.etaxonomy.cdm.api.utility.DistributionOrder;
26 import eu.etaxonomy.cdm.common.Tree;
27 import eu.etaxonomy.cdm.common.TreeNode;
28 import eu.etaxonomy.cdm.hibernate.HibernateProxyHelper;
29 import eu.etaxonomy.cdm.model.common.Marker;
30 import eu.etaxonomy.cdm.model.common.MarkerType;
31 import eu.etaxonomy.cdm.model.description.Distribution;
32 import eu.etaxonomy.cdm.model.location.NamedArea;
33 import eu.etaxonomy.cdm.model.location.NamedAreaLevel;
34 import eu.etaxonomy.cdm.persistence.dao.term.IDefinedTermDao;
35
36 /**
37 * TODO javadoc.
38 *
39 * There is a somehow similar implementation in {@link eu.etaxonomy.cdm.model.location.NamedArea}
40 */
41 public class DistributionTree extends Tree<Set<Distribution>, NamedArea>{
42
43 public static final Logger logger = Logger.getLogger(DistributionTree.class);
44
45 private final IDefinedTermDao termDao;
46
47 public DistributionTree(IDefinedTermDao termDao){
48 TreeNode<Set<Distribution>, NamedArea> rootElement = new TreeNode<Set<Distribution>, NamedArea>();
49 List<TreeNode<Set<Distribution>, NamedArea>> children = new ArrayList<TreeNode<Set<Distribution>, NamedArea>>();
50 rootElement.setChildren(children);
51 setRootElement(rootElement);
52 this.termDao = termDao;
53 }
54
55 /**
56 * @param parentNode
57 * @param nodeToFind
58 * @return false if the node was not found
59 */
60 public boolean hasChildNode(TreeNode<Set<Distribution>, NamedArea> parentNode, NamedArea nodeID) {
61 return findChildNode(parentNode, nodeID) != null;
62 }
63
64 /**
65 * Returns the (first) child node (of type TreeNode) with the given nodeID.
66 * @param parentNode
67 * @param nodeToFind
68 * @return the found node or null
69 */
70 public TreeNode<Set<Distribution>, NamedArea> findChildNode(TreeNode<Set<Distribution>, NamedArea> parentNode, NamedArea nodeID) {
71 if (parentNode.getChildren() == null) {
72 return null;
73 }
74
75 for (TreeNode<Set<Distribution>, NamedArea> node : parentNode.getChildren()) {
76 if (node.getNodeId().equals(nodeID)) {
77 return node;
78 }
79 }
80 return null;
81 }
82
83 /**
84 * @param distList
85 * @param omitLevels
86 * @param hiddenAreaMarkerTypes
87 * Areas not associated to a Distribution in the {@code distList} are detected as fall back area
88 * if they are having a {@link Marker} with one of the specified {@link MarkerType}s. Areas identified
89 * as such are omitted from the hierarchy and the sub areas are moving one level up.
90 * For more details on fall back areas see <b>Marked area filter</b> of
91 * {@link DescriptionUtility#filterDistributions(Collection, Set, boolean, boolean, boolean)}.
92 */
93 public void orderAsTree(Collection<Distribution> distList,
94 Set<NamedAreaLevel> omitLevels,
95 Set<MarkerType> hiddenAreaMarkerTypes){
96
97 Set<NamedArea> areas = new HashSet<NamedArea>(distList.size());
98 for (Distribution distribution : distList) {
99 areas.add(distribution.getArea());
100 }
101 // preload all areas which are a parent of another one, this is a performance improvement
102 loadAllParentAreas(areas);
103
104 Set<Integer> omitLevelIds = new HashSet<Integer>(omitLevels.size());
105 for(NamedAreaLevel level : omitLevels) {
106 omitLevelIds.add(level.getId());
107 }
108
109 for (Distribution distribution : distList) {
110 // get path through area hierarchy
111 List<NamedArea> namedAreaPath = getAreaLevelPath(distribution.getArea(), omitLevelIds, areas, hiddenAreaMarkerTypes);
112 addDistributionToSubTree(distribution, namedAreaPath, this.getRootElement());
113 }
114 }
115
116 /**
117 * This method will cause all parent areas to be loaded into the session cache to that
118 * all initialization of the NamedArea term instances in necessary. This improves the
119 * performance of the tree building
120 */
121 private void loadAllParentAreas(Set<NamedArea> areas) {
122
123 List<NamedArea> parentAreas = null;
124 Set<NamedArea> childAreas = new HashSet<NamedArea>(areas.size());
125 for(NamedArea areaProxy : areas) {
126 NamedArea deproxied = HibernateProxyHelper.deproxy(areaProxy, NamedArea.class);
127 childAreas.add(deproxied);
128 }
129
130 if(!childAreas.isEmpty()) {
131 parentAreas = termDao.getPartOf(childAreas, null, null, null);
132 childAreas.clear();
133 childAreas.addAll(parentAreas);
134 }
135
136 }
137
138 public void recursiveSortChildren(DistributionOrder distributionOrder){
139 if (distributionOrder == null){
140 distributionOrder = DistributionOrder.getDefault();
141 }
142 _recursiveSortChildren(this.getRootElement(), distributionOrder.getComparator());
143 }
144
145 private void _recursiveSortChildren(TreeNode<Set<Distribution>, NamedArea> treeNode,
146 Comparator<TreeNode<Set<Distribution>, NamedArea>> comparator){
147 if (treeNode.children == null) {
148 //nothing => stop condition
149 return;
150 }else {
151 Collections.sort(treeNode.children, comparator);
152 for (TreeNode<Set<Distribution>, NamedArea> child : treeNode.children) {
153 _recursiveSortChildren(child, comparator);
154 }
155 }
156 }
157
158 /**
159 * Adds the given <code>distributionElement</code> to the sub tree defined by
160 * the <code>root</code>.
161 *
162 * @param distribution
163 * the {@link Distribution} to add to the tree at the position
164 * according to the NamedArea hierarchy.
165 * @param namedAreaPath
166 * the path to the root of the NamedArea hierarchy starting the
167 * area used in the given <code>distributionElement</code>. The
168 * hierarchy is defined by the {@link NamedArea#getPartOf()}
169 * relationships
170 * @param root
171 * root element of the sub tree to which the
172 * <code>distributionElement</code> is to be added
173 */
174 private void addDistributionToSubTree(Distribution distribution,
175 List<NamedArea> namedAreaPath,
176 TreeNode<Set<Distribution>, NamedArea> root){
177
178
179 //if the list to merge is empty finish the execution
180 if (namedAreaPath.isEmpty()) {
181 return;
182 }
183
184 //getting the highest area and inserting it into the tree
185 NamedArea highestArea = namedAreaPath.get(0);
186
187
188 TreeNode<Set<Distribution>, NamedArea> child = findChildNode(root, highestArea);
189 if (child == null) {
190 // the highestDistNode is not yet in the set of children, so we add it
191 child = new TreeNode<Set<Distribution>, NamedArea>(highestArea);
192 child.setData(new HashSet<Distribution>());
193 root.addChild(child);
194 }
195
196 // add another element to the list of data
197 if(namedAreaPath.get(0).equals(distribution.getArea())){
198 if(namedAreaPath.size() > 1){
199 logger.error("there seems to be something wrong with the area hierarchy");
200 }
201 child.getData().add(distribution);
202 return; // done!
203 }
204
205 // Recursively proceed into the namedAreaPath to merge the next node
206 List<NamedArea> newList = namedAreaPath.subList(1, namedAreaPath.size());
207 addDistributionToSubTree(distribution, newList, child);
208 }
209
210
211 /**
212 * Areas for which no distribution data is available and which are marked as hidden (Marker) are omitted, see #5112
213 *
214 * @param area
215 * @param distributionAreas the areas for which distribution data exists (after filtering by
216 * {@link eu.etaxonomy.cdm.api.utility.DescriptionUtility#filterDistributions()} )
217 * @param hiddenAreaMarkerTypes
218 * Areas not associated to a Distribution in the {@code distList} are detected as fall back area
219 * if they are having a {@link Marker} with one of the specified {@link MarkerType}s. Areas identified as such
220 * are omitted. For more details on fall back areas see <b>Marked area filter</b> of
221 * {@link DescriptionUtility#filterDistributions(Collection, Set, boolean, boolean, boolean)}.
222 * @param omitLevels
223 * @return the path through area hierarchy from the <code>area</code> given as parameter to the root
224 */
225 private List<NamedArea> getAreaLevelPath(NamedArea area, Set<Integer> omitLevelIds, Set<NamedArea> distributionAreas, Set<MarkerType> hiddenAreaMarkerTypes){
226 List<NamedArea> result = new ArrayList<NamedArea>();
227 if (!matchesLevels(area, omitLevelIds)){
228 result.add(area);
229 }
230 // logging special case in order to help solving ticket #3891 (ordered distributions provided by portal/description/${uuid}/DistributionTree randomly broken)
231
232 if(area.getPartOf() == null) {
233 StringBuilder hibernateInfo = new StringBuilder();
234 hibernateInfo.append(", area is of type: ").append(area.getClass());
235 if(area instanceof HibernateProxy){
236 hibernateInfo.append(" target object is ").append(HibernateProxyHelper.getClassWithoutInitializingProxy(area));
237 }
238 logger.warn("area.partOf is NULL for " + area.getLabel() + hibernateInfo.toString());
239 }
240 while (area.getPartOf() != null) {
241 area = area.getPartOf();
242 if (!matchesLevels(area, omitLevelIds)){
243 if(!distributionAreas.contains(area) &&
244 DescriptionUtility.checkAreaMarkedHidden(hiddenAreaMarkerTypes, area)) {
245 if(logger.isDebugEnabled()) {
246 logger.debug("positive fallback area detection, skipping " + area );
247 }
248 } else {
249 result.add(0, area);
250 }
251 }
252 }
253
254 return result;
255 }
256
257 /**
258 * @param area
259 * @param levels
260 * @return
261 */
262 private boolean matchesLevels(NamedArea area, Set<Integer> omitLevelIds) {
263 if(omitLevelIds.isEmpty()) {
264 return false;
265 }
266 Serializable areaLevelId;
267 NamedAreaLevel areaLevel = area.getLevel();
268 if (areaLevel instanceof HibernateProxy) {
269 areaLevelId = ((HibernateProxy) areaLevel).getHibernateLazyInitializer().getIdentifier();
270 } else {
271 areaLevelId = areaLevel==null ? null : areaLevel.getId();
272 }
273 return omitLevelIds.contains(areaLevelId);
274 }
275
276
277
278 }