2 * Copyright (C) 2007 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
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.
10 package eu
.etaxonomy
.cdm
.api
.service
;
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
;
21 import org
.apache
.log4j
.Logger
;
22 import org
.hibernate
.proxy
.HibernateProxy
;
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
;
39 * There is a somehow similar implementation in {@link eu.etaxonomy.cdm.model.location.NamedArea}
41 public class DistributionTree
extends Tree
<Set
<Distribution
>, NamedArea
>{
43 public static final Logger logger
= Logger
.getLogger(DistributionTree
.class);
45 private final IDefinedTermDao termDao
;
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
;
58 * @return false if the node was not found
60 public boolean hasChildNode(TreeNode
<Set
<Distribution
>, NamedArea
> parentNode
, NamedArea nodeID
) {
61 return findChildNode(parentNode
, nodeID
) != null;
65 * Returns the (first) child node (of type TreeNode) with the given nodeID.
68 * @return the found node or null
70 public TreeNode
<Set
<Distribution
>, NamedArea
> findChildNode(TreeNode
<Set
<Distribution
>, NamedArea
> parentNode
, NamedArea nodeID
) {
71 if (parentNode
.getChildren() == null) {
75 for (TreeNode
<Set
<Distribution
>, NamedArea
> node
: parentNode
.getChildren()) {
76 if (node
.getNodeId().equals(nodeID
)) {
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)}.
93 public void orderAsTree(Collection
<Distribution
> distList
,
94 Set
<NamedAreaLevel
> omitLevels
,
95 Set
<MarkerType
> hiddenAreaMarkerTypes
){
97 Set
<NamedArea
> areas
= new HashSet
<NamedArea
>(distList
.size());
98 for (Distribution distribution
: distList
) {
99 areas
.add(distribution
.getArea());
101 // preload all areas which are a parent of another one, this is a performance improvement
102 loadAllParentAreas(areas
);
104 Set
<Integer
> omitLevelIds
= new HashSet
<Integer
>(omitLevels
.size());
105 for(NamedAreaLevel level
: omitLevels
) {
106 omitLevelIds
.add(level
.getId());
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());
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
121 private void loadAllParentAreas(Set
<NamedArea
> areas
) {
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
);
130 if(!childAreas
.isEmpty()) {
131 parentAreas
= termDao
.getPartOf(childAreas
, null, null, null);
133 childAreas
.addAll(parentAreas
);
138 public void recursiveSortChildren(DistributionOrder distributionOrder
){
139 if (distributionOrder
== null){
140 distributionOrder
= DistributionOrder
.getDefault();
142 _recursiveSortChildren(this.getRootElement(), distributionOrder
.getComparator());
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
151 Collections
.sort(treeNode
.children
, comparator
);
152 for (TreeNode
<Set
<Distribution
>, NamedArea
> child
: treeNode
.children
) {
153 _recursiveSortChildren(child
, comparator
);
159 * Adds the given <code>distributionElement</code> to the sub tree defined by
160 * the <code>root</code>.
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()}
171 * root element of the sub tree to which the
172 * <code>distributionElement</code> is to be added
174 private void addDistributionToSubTree(Distribution distribution
,
175 List
<NamedArea
> namedAreaPath
,
176 TreeNode
<Set
<Distribution
>, NamedArea
> root
){
179 //if the list to merge is empty finish the execution
180 if (namedAreaPath
.isEmpty()) {
184 //getting the highest area and inserting it into the tree
185 NamedArea highestArea
= namedAreaPath
.get(0);
188 TreeNode
<Set
<Distribution
>, NamedArea
> child
= findChildNode(root
, highestArea
);
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
);
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");
201 child
.getData().add(distribution
);
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
);
212 * Areas for which no distribution data is available and which are marked as hidden (Marker) are omitted, see #5112
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)}.
223 * @return the path through area hierarchy from the <code>area</code> given as parameter to the root
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
)){
230 // logging special case in order to help solving ticket #3891 (ordered distributions provided by portal/description/${uuid}/DistributionTree randomly broken)
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
));
238 logger
.warn("area.partOf is NULL for " + area
.getLabel() + hibernateInfo
.toString());
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
);
262 private boolean matchesLevels(NamedArea area
, Set
<Integer
> omitLevelIds
) {
263 if(omitLevelIds
.isEmpty()) {
266 Serializable areaLevelId
;
267 NamedAreaLevel areaLevel
= area
.getLevel();
268 if (areaLevel
instanceof HibernateProxy
) {
269 areaLevelId
= ((HibernateProxy
) areaLevel
).getHibernateLazyInitializer().getIdentifier();
271 areaLevelId
= areaLevel
==null ?
null : areaLevel
.getId();
273 return omitLevelIds
.contains(areaLevelId
);