|
| 1 | +package my.bookshop.handlers; |
| 2 | + |
| 3 | +import java.util.ArrayDeque; |
| 4 | +import java.util.Comparator; |
| 5 | +import java.util.Deque; |
| 6 | +import java.util.HashMap; |
| 7 | +import java.util.HashSet; |
| 8 | +import java.util.List; |
| 9 | +import java.util.Map; |
| 10 | +import java.util.Set; |
| 11 | +import java.util.stream.Collectors; |
| 12 | + |
| 13 | +import org.springframework.context.annotation.Profile; |
| 14 | +import org.springframework.stereotype.Component; |
| 15 | + |
| 16 | +import com.sap.cds.Row; |
| 17 | +import com.sap.cds.ql.CQL; |
| 18 | +import com.sap.cds.ql.Select; |
| 19 | +import com.sap.cds.ql.cqn.CqnElementRef; |
| 20 | +import com.sap.cds.ql.cqn.CqnPredicate; |
| 21 | +import com.sap.cds.ql.cqn.CqnSelect; |
| 22 | +import com.sap.cds.ql.cqn.CqnValue; |
| 23 | +import com.sap.cds.ql.cqn.Modifier; |
| 24 | +import com.sap.cds.ql.cqn.transformation.CqnTopLevelsTransformation; |
| 25 | +import com.sap.cds.ql.cqn.transformation.CqnAncestorsTransformation; |
| 26 | +import com.sap.cds.ql.cqn.transformation.CqnDescendantsTransformation; |
| 27 | +import com.sap.cds.ql.cqn.transformation.CqnFilterTransformation; |
| 28 | +import com.sap.cds.ql.cqn.transformation.CqnSearchTransformation; |
| 29 | +import com.sap.cds.ql.cqn.transformation.CqnOrderByTransformation; |
| 30 | +import com.sap.cds.ql.cqn.transformation.CqnTransformation; |
| 31 | +import com.sap.cds.services.cds.CdsReadEventContext; |
| 32 | +import com.sap.cds.services.cds.CqnService; |
| 33 | +import com.sap.cds.services.handler.EventHandler; |
| 34 | +import com.sap.cds.services.handler.annotations.Before; |
| 35 | +import com.sap.cds.services.handler.annotations.ServiceName; |
| 36 | +import com.sap.cds.services.persistence.PersistenceService; |
| 37 | + |
| 38 | +import cds.gen.adminservice.AdminService_; |
| 39 | +import cds.gen.adminservice.GenreHierarchy; |
| 40 | +import cds.gen.adminservice.GenreHierarchy_; |
| 41 | + |
| 42 | +@Component |
| 43 | +@Profile("default") |
| 44 | +@ServiceName(AdminService_.CDS_NAME) |
| 45 | +public class HierarchyHandler implements EventHandler { |
| 46 | + |
| 47 | + private final PersistenceService db; |
| 48 | + |
| 49 | + HierarchyHandler(PersistenceService db) { |
| 50 | + this.db = db; |
| 51 | + } |
| 52 | + |
| 53 | + @Before(event = CqnService.EVENT_READ, entity = GenreHierarchy_.CDS_NAME) |
| 54 | + public void readGenreHierarchy(CdsReadEventContext event) { |
| 55 | + List<CqnTransformation> trafos = event.getCqn().transformations(); |
| 56 | + List<GenreHierarchy> result = null; |
| 57 | + |
| 58 | + if (trafos.size() < 1) { |
| 59 | + return; |
| 60 | + } |
| 61 | + |
| 62 | + if (getTopLevels(trafos) instanceof CqnTopLevelsTransformation topLevels) { |
| 63 | + result = topLevels(topLevels, CQL.TRUE); |
| 64 | + } else if (trafos.get(0) instanceof CqnDescendantsTransformation descendants) { |
| 65 | + result = handleDescendants(descendants); |
| 66 | + } else if (trafos.get(0) instanceof CqnAncestorsTransformation ancestors) { |
| 67 | + if (trafos.size() == 2 && trafos.get(1) instanceof CqnTopLevelsTransformation topLevels) { |
| 68 | + result = handleAncestors(ancestors, topLevels); |
| 69 | + } else if (trafos.size() == 3 && trafos.get(2) instanceof CqnTopLevelsTransformation topLevels) { |
| 70 | + result = handleAncestors(ancestors, topLevels); |
| 71 | + } |
| 72 | + } |
| 73 | + |
| 74 | + setResult(event, result); |
| 75 | + } |
| 76 | + |
| 77 | + private CqnTopLevelsTransformation getTopLevels(List<CqnTransformation> trafos) { |
| 78 | + if (trafos.get(0) instanceof CqnTopLevelsTransformation topLevels) { |
| 79 | + return topLevels; |
| 80 | + } else if (trafos.size() == 2 && trafos.get(0) instanceof CqnOrderByTransformation && trafos.get(1) instanceof CqnTopLevelsTransformation topLevels) { |
| 81 | + return topLevels; |
| 82 | + } |
| 83 | + return null; |
| 84 | + } |
| 85 | + |
| 86 | + private void setResult(CdsReadEventContext event, List<GenreHierarchy> result) { |
| 87 | + if (!result.isEmpty()) { |
| 88 | + addDrillState(result); |
| 89 | + } |
| 90 | + |
| 91 | + event.setResult(result); |
| 92 | + } |
| 93 | + |
| 94 | + private void addDrillState(List<GenreHierarchy> ghs) { |
| 95 | + List<Integer> ids = ghs.stream().map(gh -> gh.getId()).toList(); |
| 96 | + Set<Integer> parents = ghs.stream().map(gh -> gh.getParntId()).filter(p -> p != null).collect(Collectors.toSet()); |
| 97 | + CqnSelect q = Select.from(AdminService_.GENRE_HIERARCHY).columns(gh -> gh.parnt_ID().as("id")) |
| 98 | + .where(gh -> gh.parnt_ID().in(ids)); |
| 99 | + Set<Object> nonLeafs = db |
| 100 | + .run(q) |
| 101 | + .stream().map(r -> r.get("id")).collect(Collectors.toSet()); |
| 102 | + |
| 103 | + for (GenreHierarchy gh : ghs) { |
| 104 | + Integer id = gh.getId(); |
| 105 | + if (nonLeafs.contains(id)) { |
| 106 | + if (parents.contains(id)) { |
| 107 | + gh.setDrillState("expanded"); |
| 108 | + } else { |
| 109 | + gh.setDrillState("collapsed"); |
| 110 | + } |
| 111 | + } else { |
| 112 | + gh.setDrillState("leaf"); |
| 113 | + } |
| 114 | + } |
| 115 | + } |
| 116 | + |
| 117 | + private List<GenreHierarchy> handleDescendants(CqnDescendantsTransformation descendants) { |
| 118 | + Map<Integer, GenreHierarchy> lookup = new HashMap<>(); |
| 119 | + CqnFilterTransformation filter = (CqnFilterTransformation) descendants.transformations().get(0); |
| 120 | + CqnSelect getRoot = Select.from(AdminService_.GENRE_HIERARCHY).where(filter.filter()); |
| 121 | + GenreHierarchy root = db.run(getRoot).single(GenreHierarchy.class); |
| 122 | + lookup.put(root.getId(), root); |
| 123 | + |
| 124 | + CqnPredicate parentFilter = CQL.copy(filter.filter(), new Modifier() { |
| 125 | + @Override |
| 126 | + public CqnValue ref(CqnElementRef ref) { |
| 127 | + return CQL.get(GenreHierarchy.PARNT_ID); |
| 128 | + } |
| 129 | + }); |
| 130 | + |
| 131 | + CqnSelect childrenCQN = Select.from(AdminService_.GENRE_HIERARCHY).where(parentFilter); |
| 132 | + List<GenreHierarchy> children = db.run(childrenCQN).listOf(GenreHierarchy.class); |
| 133 | + children.forEach(gh -> lookup.put(gh.getId(), gh)); |
| 134 | + children.forEach(gh -> gh.setParnt(lookup.get(gh.getParntId()))); |
| 135 | + |
| 136 | + return children.stream().sorted(new Sorter()).toList(); |
| 137 | + } |
| 138 | + |
| 139 | + private List<GenreHierarchy> handleAncestors(CqnAncestorsTransformation ancestors, CqnTopLevelsTransformation topLevels) { |
| 140 | + CqnTransformation trafo = ancestors.transformations().get(0); |
| 141 | + Select<GenreHierarchy_> inner = Select.from(AdminService_.GENRE_HIERARCHY).columns(gh -> gh.ID()); |
| 142 | + if (trafo instanceof CqnFilterTransformation filter) { |
| 143 | + inner.where(filter.filter()); |
| 144 | + } else if (trafo instanceof CqnSearchTransformation search) { |
| 145 | + inner.search(search.search()); |
| 146 | + } |
| 147 | + Select<GenreHierarchy_> outer = Select.from(AdminService_.GENRE_HIERARCHY).columns(gh -> gh.ID().as("i0"), gh -> gh.parnt().ID().as("i1"), |
| 148 | + gh -> gh.parnt().parnt().ID().as("i2"), gh -> gh.parnt().parnt().parnt().ID().as("i3"), |
| 149 | + gh -> gh.parnt().parnt().parnt().parnt().ID().as("i4")).where(gh -> gh.ID().in(inner)); |
| 150 | + |
| 151 | + Set<Integer> ancestorIds = new HashSet<>(); |
| 152 | + db.run(outer).stream().forEach(r -> { |
| 153 | + addIfNotNull(ancestorIds, r, "i0"); |
| 154 | + addIfNotNull(ancestorIds, r, "i1"); |
| 155 | + addIfNotNull(ancestorIds, r, "i2"); |
| 156 | + addIfNotNull(ancestorIds, r, "i3"); |
| 157 | + addIfNotNull(ancestorIds, r, "i4"); |
| 158 | + }); |
| 159 | + |
| 160 | + CqnPredicate filter = CQL.get(GenreHierarchy_.ID).in(ancestorIds.stream().toList()); |
| 161 | + return topLevels(topLevels, filter); |
| 162 | + } |
| 163 | + |
| 164 | + private void addIfNotNull(Set<Integer> ancestorIds, Row r, String key) { |
| 165 | + Integer id = (Integer) r.get(key); |
| 166 | + if (id != null) { |
| 167 | + ancestorIds.add(id); |
| 168 | + } |
| 169 | + } |
| 170 | + |
| 171 | + private List<GenreHierarchy> topLevels(CqnTopLevelsTransformation topLevels, CqnPredicate filter) { |
| 172 | + return topLevels.levels() < 0 || !(topLevels.expandLevels().isEmpty()) ? topLevelsAll(filter) : topLevelsLimit(topLevels.levels(), filter); |
| 173 | + } |
| 174 | + |
| 175 | + private List<GenreHierarchy> topLevelsLimit(long limit, CqnPredicate filter) { |
| 176 | + Map <Integer, GenreHierarchy> lookup = new HashMap<>(); |
| 177 | + |
| 178 | + CqnSelect getRoots = Select.from(AdminService_.GENRE_HIERARCHY).where(gh -> gh.parnt_ID().isNull().and(filter)); |
| 179 | + List<GenreHierarchy> roots = db.run(getRoots).listOf(GenreHierarchy.class); |
| 180 | + roots.forEach(root -> { |
| 181 | + root.setDistanceFromRoot(0l); |
| 182 | + lookup.put(root.getId(), root); |
| 183 | + List<Integer> parents = List.of(root.getId()); |
| 184 | + for (long i = 1; i < limit; i++) { |
| 185 | + List<Integer> ps = parents; |
| 186 | + CqnSelect getChildren = Select.from(AdminService_.GENRE_HIERARCHY).where(gh -> gh.parnt_ID().in(ps).and(filter)); |
| 187 | + List<GenreHierarchy> children = db.run(getChildren).listOf(GenreHierarchy.class); |
| 188 | + if (children.isEmpty()) { |
| 189 | + break; |
| 190 | + } |
| 191 | + long dfr = i; |
| 192 | + parents = children.stream().peek(gh -> { |
| 193 | + gh.setParnt(lookup.get(gh.getParntId())); |
| 194 | + gh.setDistanceFromRoot(dfr); |
| 195 | + lookup.put(gh.getId(), gh); |
| 196 | + }).map(GenreHierarchy::getId).toList(); |
| 197 | + } |
| 198 | + }); |
| 199 | + |
| 200 | + return lookup.values().stream().sorted(new Sorter()).toList(); |
| 201 | + } |
| 202 | + |
| 203 | + private List<GenreHierarchy> topLevelsAll(CqnPredicate filter) { |
| 204 | + Map<Integer, GenreHierarchy> lookup = new HashMap<>(); |
| 205 | + |
| 206 | + CqnSelect allCqn = Select.from(AdminService_.GENRE_HIERARCHY).where(filter); |
| 207 | + var all = db.run(allCqn).listOf(GenreHierarchy.class); |
| 208 | + all.forEach(gh -> lookup.put(gh.getId(), gh)); |
| 209 | + all.forEach(gh -> gh.setParnt(lookup.get(gh.getParntId()))); |
| 210 | + all.forEach(gh -> gh.setDistanceFromRoot(distanceFromRoot(gh))); |
| 211 | + |
| 212 | + return all.stream().sorted(new Sorter()).toList(); |
| 213 | + } |
| 214 | + |
| 215 | + private static long distanceFromRoot(GenreHierarchy gh) { |
| 216 | + long dfr = 0; |
| 217 | + while (gh.getParnt() != null) { |
| 218 | + dfr++; |
| 219 | + gh = gh.getParnt(); |
| 220 | + } |
| 221 | + |
| 222 | + return dfr; |
| 223 | + } |
| 224 | + |
| 225 | + private class Sorter implements Comparator<GenreHierarchy> { |
| 226 | + |
| 227 | + @Override |
| 228 | + public int compare(GenreHierarchy gh1, GenreHierarchy gh2) { |
| 229 | + Deque<String> path1 = getPath(gh1); |
| 230 | + Deque<String> path2 = getPath(gh2); |
| 231 | + int res = 0; |
| 232 | + |
| 233 | + while (!path1.isEmpty() && !path2.isEmpty()) { |
| 234 | + String last1 = path1.pop(); |
| 235 | + String last2 = path2.pop(); |
| 236 | + res = last1.compareTo(last2); |
| 237 | + if (res != 0) { |
| 238 | + return res; |
| 239 | + } |
| 240 | + } |
| 241 | + return res; |
| 242 | + } |
| 243 | + |
| 244 | + Deque<String> getPath(GenreHierarchy gh){ |
| 245 | + Deque<String> path = new ArrayDeque<>(); |
| 246 | + do { |
| 247 | + path.push(gh.getName()); |
| 248 | + gh = gh.getParnt(); |
| 249 | + } while (gh != null); |
| 250 | + |
| 251 | + return path; |
| 252 | + } |
| 253 | + } |
| 254 | +} |
0 commit comments