2 * Copyright (C) 2023 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.
9 package eu
.etaxonomy
.cdm
.api
.service
.portal
;
11 import java
.io
.Serializable
;
12 import java
.util
.ArrayList
;
13 import java
.util
.Collection
;
14 import java
.util
.Collections
;
15 import java
.util
.Comparator
;
16 import java
.util
.HashSet
;
17 import java
.util
.List
;
19 import java
.util
.UUID
;
21 import org
.apache
.logging
.log4j
.LogManager
;
22 import org
.apache
.logging
.log4j
.Logger
;
23 import org
.hibernate
.proxy
.HibernateProxy
;
25 import eu
.etaxonomy
.cdm
.api
.dto
.portal
.DistributionDto
;
26 import eu
.etaxonomy
.cdm
.api
.dto
.portal
.DistributionTreeDto
;
27 import eu
.etaxonomy
.cdm
.api
.dto
.portal
.LabeledEntityDto
;
28 import eu
.etaxonomy
.cdm
.api
.dto
.portal
.NamedAreaDto
;
29 import eu
.etaxonomy
.cdm
.api
.dto
.portal
.config
.DistributionOrder
;
30 import eu
.etaxonomy
.cdm
.common
.SetMap
;
31 import eu
.etaxonomy
.cdm
.common
.TreeNode
;
32 import eu
.etaxonomy
.cdm
.model
.common
.Marker
;
33 import eu
.etaxonomy
.cdm
.model
.common
.MarkerType
;
34 import eu
.etaxonomy
.cdm
.model
.description
.Distribution
;
35 import eu
.etaxonomy
.cdm
.model
.location
.NamedArea
;
36 import eu
.etaxonomy
.cdm
.model
.location
.NamedAreaLevel
;
37 import eu
.etaxonomy
.cdm
.persistence
.dao
.term
.IDefinedTermDao
;
43 public class DistributionTreeDtoLoader
{
45 private static final Logger logger
= LogManager
.getLogger();
47 private final IDefinedTermDao termDao
;
49 public DistributionTreeDtoLoader(IDefinedTermDao termDao
){
50 this.termDao
= termDao
;
53 public DistributionTreeDto
load() {
54 DistributionTreeDto dto
= new DistributionTreeDto();
55 TreeNode
<Set
<DistributionDto
>, NamedAreaDto
> rootElement
= new TreeNode
<>();
56 List
<TreeNode
<Set
<DistributionDto
>, NamedAreaDto
>> children
= new ArrayList
<>();
57 rootElement
.setChildren(children
);
58 dto
.setRootElement(rootElement
);
63 * Returns the (first) child node (of type TreeNode) with the given nodeID.
64 * @return the found node or null
66 public TreeNode
<Set
<DistributionDto
>, NamedAreaDto
> findChildNode(TreeNode
<Set
<DistributionDto
>, NamedAreaDto
> parentNode
, NamedAreaDto nodeID
) {
67 if (parentNode
.getChildren() == null) {
71 for (TreeNode
<Set
<DistributionDto
>, NamedAreaDto
> node
: parentNode
.getChildren()) {
72 if (node
.getNodeId().getUuid().equals(nodeID
.getUuid())) {
80 * @param parentAreaMap
81 * @param fallbackAreaMarkerTypes
82 * Areas are fallback areas if they have a {@link Marker} with one of the specified
83 * {@link MarkerType marker types}.
84 * Areas identified as such are omitted from the hierarchy and the sub areas are moving one level up.
85 * This may not be the case if the fallback area has a distribution record itself AND if
86 * neverUseFallbackAreasAsParents is <code>false</code>.
87 * For more details on fall back areas see <b>Marked area filter</b> of
88 * {@link DescriptionUtility#filterDistributions(Collection, Set, boolean, boolean, boolean)}.
89 * @param neverUseFallbackAreasAsParents
90 * if <code>true</code> a fallback area never has children even if a record exists for the area
92 public void orderAsTree(DistributionTreeDto dto
, Collection
<DistributionDto
> distributions
,
93 SetMap
<NamedArea
, NamedArea
> parentAreaMap
, Set
<NamedAreaLevel
> omitLevels
,
94 Set
<MarkerType
> fallbackAreaMarkerTypes
,
95 boolean neverUseFallbackAreasAsParents
){
98 Set
<NamedAreaDto
> relevantAreas
= new HashSet
<>(distributions
.size());
99 for (DistributionDto distribution
: distributions
) {
100 relevantAreas
.add(distribution
.getArea());
102 // preload all areas which are a parent of another one, this is a performance improvement
103 loadAllParentAreasIntoSession(relevantAreas
, parentAreaMap
);
105 Set
<Integer
> omitLevelIds
= new HashSet
<>(omitLevels
.size());
106 for(NamedAreaLevel level
: omitLevels
) {
107 omitLevelIds
.add(level
.getId());
110 for (DistributionDto distribution
: distributions
) {
111 // get path through area hierarchy
112 List
<NamedAreaDto
> namedAreaPath
= getAreaLevelPath(distribution
.getArea(), parentAreaMap
,
113 omitLevelIds
, relevantAreas
, fallbackAreaMarkerTypes
, neverUseFallbackAreasAsParents
);
114 addDistributionToSubTree(distribution
, namedAreaPath
, dto
.getRootElement());
119 * This method will cause all parent areas to be loaded into the session cache so that
120 * all initialization of the NamedArea term instances is ready. This improves the
121 * performance of the tree building
123 private void loadAllParentAreasIntoSession(Set
<NamedAreaDto
> areas
, SetMap
<NamedArea
, NamedArea
> parentAreaMap
) {
125 List
<NamedAreaDto
> parentAreas
= null;
126 Set
<UUID
> childAreas
= new HashSet
<>(areas
.size());
127 for(NamedAreaDto area
: areas
) {
128 childAreas
.add(area
.getUuid());
131 if(!childAreas
.isEmpty()) {
132 parentAreas
= termDao
.getPartOfNamedAreas(childAreas
, parentAreaMap
);
134 // cdhildAreas.addAll(parentAreas);
138 public void recursiveSortChildren(DistributionTreeDto dto
, DistributionOrder distributionOrder
){
139 if (distributionOrder
== null){
140 distributionOrder
= DistributionOrder
.getDefault();
142 innerRecursiveSortChildren(dto
.getRootElement(), distributionOrder
.getDtoComparator());
145 private void innerRecursiveSortChildren(TreeNode
<Set
<DistributionDto
>, NamedAreaDto
> treeNode
,
146 Comparator
<TreeNode
<Set
<DistributionDto
>, NamedAreaDto
>> comparator
){
148 if (treeNode
.children
== null) {
149 //nothing => stop condition
152 Collections
.sort(treeNode
.getChildren(), comparator
);
153 for (TreeNode
<Set
<DistributionDto
>, NamedAreaDto
> child
: treeNode
.getChildren()) {
154 innerRecursiveSortChildren(child
, comparator
);
160 * Adds the given <code>distributionElement</code> to the sub tree defined by
161 * the <code>root</code>.
163 * @param distribution
164 * the {@link Distribution} to add to the tree at the position
165 * according to the NamedArea hierarchy.
166 * @param namedAreaPath
167 * the path to the root of the NamedArea hierarchy starting the
168 * area used in the given <code>distributionElement</code>. The
169 * hierarchy is defined by the {@link NamedArea#getPartOf()}
172 * root element of the sub tree to which the
173 * <code>distributionElement</code> is to be added
175 private void addDistributionToSubTree(DistributionDto distribution
,
176 List
<NamedAreaDto
> namedAreaPath
,
177 TreeNode
<Set
<DistributionDto
>, NamedAreaDto
> root
){
180 //if the list to merge is empty finish the execution
181 if (namedAreaPath
.isEmpty()) {
185 //getting the highest area and inserting it into the tree
186 NamedAreaDto highestArea
= namedAreaPath
.get(0);
188 TreeNode
<Set
<DistributionDto
>, NamedAreaDto
> child
= findChildNode(root
, highestArea
);
190 // the highestDistNode is not yet in the set of children, so we add it
191 child
= new TreeNode
<Set
<DistributionDto
>, NamedAreaDto
>(highestArea
);
192 child
.setData(new HashSet
<DistributionDto
>());
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
<NamedAreaDto
> newList
= namedAreaPath
.subList(1, namedAreaPath
.size());
207 addDistributionToSubTree(distribution
, newList
, child
);
211 * Returns the path through the area hierarchy from the root area to the <code>area</code> given as parameter.<BR><BR>
213 * Areas for which no distribution data is available and which are marked as hidden are omitted, see #5112
216 * @param parentAreaMap
217 * @param distributionAreas the areas for which distribution data exists (after filtering by
218 * {@link eu.etaxonomy.cdm.api.service.geo.DescriptionUtility#filterDistributions()} )
219 * @param fallbackAreaMarkerTypes
220 * Areas not associated to a Distribution in the {@code distList} are detected as fallback area
221 * if they are having a {@link Marker} with one of the specified {@link MarkerType}s. Areas identified as such
222 * are omitted. For more details on fall back areas see <b>Marked area filter</b> of
223 * {@link DescriptionUtility#filterDistributions(Collection, Set, boolean, boolean, boolean)}.
225 * @return the path through the area hierarchy
227 private List
<NamedAreaDto
> getAreaLevelPath(NamedAreaDto area
, SetMap
<NamedArea
, NamedArea
> parentAreaMap
,
228 Set
<Integer
> omitLevelIds
,
229 Set
<NamedAreaDto
> distributionAreas
, Set
<MarkerType
> fallbackAreaMarkerTypes
,
230 boolean neverUseFallbackAreasAsParents
){
232 List
<NamedAreaDto
> result
= new ArrayList
<>();
233 if (!matchesLevels(area
, omitLevelIds
)){
237 if (parentAreaMap
== null) { //TODO should this happen?
238 while (area
.getParent() != null) {
239 area
= area
.getParent();
240 if (!matchesLevels(area
, omitLevelIds
)){
241 if(!isFallback(fallbackAreaMarkerTypes
, area
) ||
242 (distributionAreas
.contains(area
) && !neverUseFallbackAreasAsParents
) ) {
245 if(logger
.isDebugEnabled()) {logger
.debug("positive fallback area detection, skipping " + area
);}
250 //FIXME same as above case, maybe we do not need to distinguish as parent handling is done
251 // in NamedAreaDTO constructor
252 while (area
.getParent() != null) {
253 area
= area
.getParent();
254 if (!matchesLevels(area
, omitLevelIds
)){
255 if(!isFallback(fallbackAreaMarkerTypes
, area
) ||
256 (distributionAreas
.contains(area
) && !neverUseFallbackAreasAsParents
) ) {
259 if(logger
.isDebugEnabled()) {logger
.debug("positive fallback area detection, skipping " + area
);}
268 private boolean isFallback(Set
<MarkerType
> hiddenAreaMarkerTypes
, NamedAreaDto area
) {
270 //was: DescriptionUtility.isMarkedHidden(area, hiddenAreaMarkerTypes);
271 return isMarkedHidden(area
, hiddenAreaMarkerTypes
);
274 private static boolean isMarkedHidden(NamedAreaDto area
, Set
<MarkerType
> hiddenAreaMarkerTypes
) {
275 if(hiddenAreaMarkerTypes
!= null) {
276 for(MarkerType markerType
: hiddenAreaMarkerTypes
){
277 if(area
.hasMarker(markerType
, true)){
285 private boolean matchesLevels(NamedAreaDto area
, Set
<Integer
> omitLevelIds
) {
286 if(omitLevelIds
.isEmpty()) {
289 Serializable areaLevelId
;
290 LabeledEntityDto areaLevel
= area
.getLevel();
291 //TODO remove Proxy check
292 if (areaLevel
instanceof HibernateProxy
) {
293 areaLevelId
= ((HibernateProxy
) areaLevel
).getHibernateLazyInitializer().getIdentifier();
295 areaLevelId
= areaLevel
==null ?
null : areaLevel
.getId();
297 return omitLevelIds
.contains(areaLevelId
);