ref #10222 fix equals in distribution tree loader
[cdmlib.git] / cdmlib-services / src / main / java / eu / etaxonomy / cdm / api / service / portal / DistributionTreeDtoLoader.java
1 /**
2 * Copyright (C) 2023 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 package eu.etaxonomy.cdm.api.service.portal;
10
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;
18 import java.util.Set;
19 import java.util.UUID;
20
21 import org.apache.logging.log4j.LogManager;
22 import org.apache.logging.log4j.Logger;
23 import org.hibernate.proxy.HibernateProxy;
24
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;
38
39 /**
40 * @author a.mueller
41 * @date 09.02.2023
42 */
43 public class DistributionTreeDtoLoader {
44
45 private static final Logger logger = LogManager.getLogger();
46
47 private final IDefinedTermDao termDao;
48
49 public DistributionTreeDtoLoader(IDefinedTermDao termDao){
50 this.termDao = termDao;
51 }
52
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);
59 return dto;
60 }
61
62 /**
63 * Returns the (first) child node (of type TreeNode) with the given nodeID.
64 * @return the found node or null
65 */
66 public TreeNode<Set<DistributionDto>, NamedAreaDto> findChildNode(TreeNode<Set<DistributionDto>, NamedAreaDto> parentNode, NamedAreaDto nodeID) {
67 if (parentNode.getChildren() == null) {
68 return null;
69 }
70
71 for (TreeNode<Set<DistributionDto>, NamedAreaDto> node : parentNode.getChildren()) {
72 if (node.getNodeId().getUuid().equals(nodeID.getUuid())) {
73 return node;
74 }
75 }
76 return null;
77 }
78
79 /**
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
91 */
92 public void orderAsTree(DistributionTreeDto dto, Collection<DistributionDto> distributions,
93 SetMap<NamedArea, NamedArea> parentAreaMap, Set<NamedAreaLevel> omitLevels,
94 Set<MarkerType> fallbackAreaMarkerTypes,
95 boolean neverUseFallbackAreasAsParents){
96
97 //compute all areas
98 Set<NamedAreaDto> relevantAreas = new HashSet<>(distributions.size());
99 for (DistributionDto distribution : distributions) {
100 relevantAreas.add(distribution.getArea());
101 }
102 // preload all areas which are a parent of another one, this is a performance improvement
103 loadAllParentAreasIntoSession(relevantAreas, parentAreaMap);
104
105 Set<Integer> omitLevelIds = new HashSet<>(omitLevels.size());
106 for(NamedAreaLevel level : omitLevels) {
107 omitLevelIds.add(level.getId());
108 }
109
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());
115 }
116 }
117
118 /**
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
122 */
123 private void loadAllParentAreasIntoSession(Set<NamedAreaDto> areas, SetMap<NamedArea, NamedArea> parentAreaMap) {
124
125 List<NamedAreaDto> parentAreas = null;
126 Set<UUID> childAreas = new HashSet<>(areas.size());
127 for(NamedAreaDto area : areas) {
128 childAreas.add(area.getUuid());
129 }
130
131 if(!childAreas.isEmpty()) {
132 parentAreas = termDao.getPartOfNamedAreas(childAreas, parentAreaMap);
133 childAreas.clear();
134 // cdhildAreas.addAll(parentAreas);
135 }
136 }
137
138 public void recursiveSortChildren(DistributionTreeDto dto, DistributionOrder distributionOrder){
139 if (distributionOrder == null){
140 distributionOrder = DistributionOrder.getDefault();
141 }
142 innerRecursiveSortChildren(dto.getRootElement(), distributionOrder.getDtoComparator());
143 }
144
145 private void innerRecursiveSortChildren(TreeNode<Set<DistributionDto>, NamedAreaDto> treeNode,
146 Comparator<TreeNode<Set<DistributionDto>, NamedAreaDto>> comparator){
147
148 if (treeNode.children == null) {
149 //nothing => stop condition
150 return;
151 }else {
152 Collections.sort(treeNode.getChildren(), comparator);
153 for (TreeNode<Set<DistributionDto>, NamedAreaDto> child : treeNode.getChildren()) {
154 innerRecursiveSortChildren(child, comparator);
155 }
156 }
157 }
158
159 /**
160 * Adds the given <code>distributionElement</code> to the sub tree defined by
161 * the <code>root</code>.
162 *
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()}
170 * relationships
171 * @param root
172 * root element of the sub tree to which the
173 * <code>distributionElement</code> is to be added
174 */
175 private void addDistributionToSubTree(DistributionDto distribution,
176 List<NamedAreaDto> namedAreaPath,
177 TreeNode<Set<DistributionDto>, NamedAreaDto> root){
178
179
180 //if the list to merge is empty finish the execution
181 if (namedAreaPath.isEmpty()) {
182 return;
183 }
184
185 //getting the highest area and inserting it into the tree
186 NamedAreaDto highestArea = namedAreaPath.get(0);
187
188 TreeNode<Set<DistributionDto>, NamedAreaDto> 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<DistributionDto>, NamedAreaDto>(highestArea);
192 child.setData(new HashSet<DistributionDto>());
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<NamedAreaDto> newList = namedAreaPath.subList(1, namedAreaPath.size());
207 addDistributionToSubTree(distribution, newList, child);
208 }
209
210 /**
211 * Returns the path through the area hierarchy from the root area to the <code>area</code> given as parameter.<BR><BR>
212 *
213 * Areas for which no distribution data is available and which are marked as hidden are omitted, see #5112
214 *
215 * @param area
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)}.
224 * @param omitLevels
225 * @return the path through the area hierarchy
226 */
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){
231
232 List<NamedAreaDto> result = new ArrayList<>();
233 if (!matchesLevels(area, omitLevelIds)){
234 result.add(area);
235 }
236
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 ) ) {
243 result.add(0, area);
244 } else {
245 if(logger.isDebugEnabled()) {logger.debug("positive fallback area detection, skipping " + area );}
246 }
247 }
248 }
249 } else {
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 ) ) {
257 result.add(0, area);
258 } else {
259 if(logger.isDebugEnabled()) {logger.debug("positive fallback area detection, skipping " + area );}
260 }
261 }
262 }
263
264 }
265 return result;
266 }
267
268 private boolean isFallback(Set<MarkerType> hiddenAreaMarkerTypes, NamedAreaDto area) {
269
270 //was: DescriptionUtility.isMarkedHidden(area, hiddenAreaMarkerTypes);
271 return isMarkedHidden(area, hiddenAreaMarkerTypes);
272 }
273
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)){
278 return true;
279 }
280 }
281 }
282 return false;
283 }
284
285 private boolean matchesLevels(NamedAreaDto area, Set<Integer> omitLevelIds) {
286 if(omitLevelIds.isEmpty()) {
287 return false;
288 }
289 Serializable areaLevelId;
290 LabeledEntityDto areaLevel = area.getLevel();
291 //TODO remove Proxy check
292 if (areaLevel instanceof HibernateProxy) {
293 areaLevelId = ((HibernateProxy) areaLevel).getHibernateLazyInitializer().getIdentifier();
294 } else {
295 areaLevelId = areaLevel==null ? null : areaLevel.getId();
296 }
297 return omitLevelIds.contains(areaLevelId);
298 }
299 }