1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.mycore.datamodel.classifications2.impl;
20
21 import java.util.BitSet;
22 import java.util.Collection;
23 import java.util.HashMap;
24 import java.util.HashSet;
25 import java.util.LinkedList;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Optional;
29 import java.util.stream.Collectors;
30
31 import org.apache.logging.log4j.LogManager;
32 import org.apache.logging.log4j.Logger;
33 import org.hibernate.annotations.QueryHints;
34 import org.mycore.backend.jpa.MCREntityManagerProvider;
35 import org.mycore.common.MCRCache;
36 import org.mycore.common.MCRPersistenceException;
37 import org.mycore.common.MCRStreamUtils;
38 import org.mycore.common.config.MCRConfiguration2;
39 import org.mycore.datamodel.classifications2.MCRCategLinkReference;
40 import org.mycore.datamodel.classifications2.MCRCategLinkReference_;
41 import org.mycore.datamodel.classifications2.MCRCategLinkService;
42 import org.mycore.datamodel.classifications2.MCRCategory;
43 import org.mycore.datamodel.classifications2.MCRCategoryDAO;
44 import org.mycore.datamodel.classifications2.MCRCategoryDAOFactory;
45 import org.mycore.datamodel.classifications2.MCRCategoryID;
46 import org.mycore.datamodel.classifications2.MCRCategoryLink;
47
48 import jakarta.persistence.EntityManager;
49 import jakarta.persistence.Query;
50 import jakarta.persistence.TypedQuery;
51 import jakarta.persistence.criteria.CriteriaBuilder;
52 import jakarta.persistence.criteria.CriteriaQuery;
53 import jakarta.persistence.criteria.Path;
54 import jakarta.persistence.criteria.Root;
55
56
57
58
59
60
61
62
63 public class MCRCategLinkServiceImpl implements MCRCategLinkService {
64
65 private static Logger LOGGER = LogManager.getLogger();
66
67 private static Class<MCRCategoryLinkImpl> LINK_CLASS = MCRCategoryLinkImpl.class;
68
69 private static final String NAMED_QUERY_NAMESPACE = "MCRCategoryLink.";
70
71 private static MCRCache<MCRCategoryID, MCRCategory> categCache = new MCRCache<>(
72 MCRConfiguration2.getInt("MCR.Classifications.LinkServiceImpl.CategCache.Size").orElse(1000),
73 "MCRCategLinkService category cache");
74
75 private static MCRCategoryDAO DAO = MCRCategoryDAOFactory.getInstance();
76
77 @Override
78 public Map<MCRCategoryID, Number> countLinks(MCRCategory parent, boolean childrenOnly) {
79 return countLinksForType(parent, null, childrenOnly);
80 }
81
82 @Override
83 public Map<MCRCategoryID, Number> countLinksForType(MCRCategory parent, String type, boolean childrenOnly) {
84 boolean restrictedByType = type != null;
85 String queryName;
86 if (childrenOnly) {
87 queryName = restrictedByType ? "NumberByTypePerChildOfParentID" : "NumberPerChildOfParentID";
88 } else {
89 queryName = restrictedByType ? "NumberByTypePerClassID" : "NumberPerClassID";
90 }
91 Map<MCRCategoryID, Number> countLinks = new HashMap<>();
92 Collection<MCRCategoryID> ids = childrenOnly ? getAllChildIDs(parent) : getAllCategIDs(parent);
93 for (MCRCategoryID id : ids) {
94
95 countLinks.put(id, 0);
96 }
97
98
99 final EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
100 if (!childrenOnly) {
101 parent = parent.getRoot();
102 } else if (!(parent instanceof MCRCategoryImpl) || ((MCRCategoryImpl) parent).getInternalID() == 0) {
103 parent = MCRCategoryDAOImpl.getByNaturalID(em, parent.getId());
104 }
105 LOGGER.info("parentID:{}", parent.getId());
106 String classID = parent.getId().getRootID();
107 TypedQuery<Object[]> q = em.createNamedQuery(NAMED_QUERY_NAMESPACE + queryName, Object[].class);
108
109 setCacheable(q);
110 setReadOnly(q);
111 q.setParameter("classID", classID);
112 if (childrenOnly) {
113 q.setParameter("parentID", ((MCRCategoryImpl) parent).getInternalID());
114 }
115 if (restrictedByType) {
116 q.setParameter("type", type);
117 }
118
119 List<Object[]> result = q.getResultList();
120 for (Object[] sr : result) {
121 MCRCategoryID key = new MCRCategoryID(classID, sr[0].toString());
122 Number value = (Number) sr[1];
123 countLinks.put(key, value);
124 }
125 return countLinks;
126 }
127
128 @Override
129 public void deleteLink(MCRCategLinkReference reference) {
130 final EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
131 Query q = em.createNamedQuery(NAMED_QUERY_NAMESPACE + "deleteByObjectID");
132 q.setParameter("id", reference.getObjectID());
133 q.setParameter("type", reference.getType());
134 int deleted = q.executeUpdate();
135 LOGGER.debug("Number of Links deleted: {}", deleted);
136 }
137
138 @Override
139 public void deleteLinks(final Collection<MCRCategLinkReference> ids) {
140 if (ids.isEmpty()) {
141 return;
142 }
143 HashMap<String, Collection<String>> typeMap = new HashMap<>();
144
145 Collection<String> objectIds = new LinkedList<>();
146 String currentType = ids.iterator().next().getType();
147 typeMap.put(currentType, objectIds);
148
149 for (MCRCategLinkReference ref : ids) {
150 if (!currentType.equals(ref.getType())) {
151 currentType = ref.getType();
152 objectIds = typeMap.computeIfAbsent(ref.getType(), k -> new LinkedList<>());
153 }
154 objectIds.add(ref.getObjectID());
155 }
156 EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
157 jakarta.persistence.Query q = em.createNamedQuery(NAMED_QUERY_NAMESPACE + "deleteByObjectCollection");
158 int deleted = 0;
159 for (Map.Entry<String, Collection<String>> entry : typeMap.entrySet()) {
160 q.setParameter("ids", entry.getValue());
161 q.setParameter("type", entry.getKey());
162 deleted += q.executeUpdate();
163 }
164 LOGGER.debug("Number of Links deleted: {}", deleted);
165 }
166
167 @Override
168 public Collection<String> getLinksFromCategory(MCRCategoryID id) {
169 final EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
170 TypedQuery<String> q = em.createNamedQuery(NAMED_QUERY_NAMESPACE + "ObjectIDByCategory", String.class);
171 setCacheable(q);
172 q.setParameter("id", id);
173 setReadOnly(q);
174 return q.getResultList();
175 }
176
177 @Override
178 public Collection<String> getLinksFromCategoryForType(MCRCategoryID id, String type) {
179 final EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
180 TypedQuery<String> q = em.createNamedQuery(NAMED_QUERY_NAMESPACE + "ObjectIDByCategoryAndType", String.class);
181 setCacheable(q);
182 q.setParameter("id", id);
183 q.setParameter("type", type);
184 setReadOnly(q);
185 return q.getResultList();
186 }
187
188 @Override
189 public Collection<MCRCategoryID> getLinksFromReference(MCRCategLinkReference reference) {
190 final EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
191 TypedQuery<MCRCategoryID> q = em.createNamedQuery(NAMED_QUERY_NAMESPACE + "categoriesByObjectID",
192 MCRCategoryID.class);
193 setCacheable(q);
194 q.setParameter("id", reference.getObjectID());
195 q.setParameter("type", reference.getType());
196 setReadOnly(q);
197 return q.getResultList();
198 }
199
200 @Override
201 public void setLinks(MCRCategLinkReference objectReference, Collection<MCRCategoryID> categories) {
202 EntityManager entityManager = MCREntityManagerProvider.getCurrentEntityManager();
203 categories
204 .stream()
205 .distinct()
206 .forEach(categID -> {
207 final MCRCategory category = getMCRCategory(entityManager, categID);
208 if (category == null) {
209 throw new MCRPersistenceException("Could not link to unknown category " + categID);
210 }
211 MCRCategoryLinkImpl link = new MCRCategoryLinkImpl(category, objectReference);
212 if (LOGGER.isDebugEnabled()) {
213 MCRCategory linkedCategory = link.getCategory();
214 StringBuilder debugMessage = new StringBuilder("Adding Link from ").append(linkedCategory.getId());
215 if (linkedCategory instanceof MCRCategoryImpl) {
216 debugMessage.append("(").append(((MCRCategoryImpl) linkedCategory).getInternalID())
217 .append(") ");
218 }
219 debugMessage.append("to ").append(objectReference);
220 LOGGER.debug(debugMessage.toString());
221 }
222 entityManager.persist(link);
223 LOGGER.debug("===DONE: {}", link.id);
224 });
225 }
226
227 private static MCRCategory getMCRCategory(EntityManager entityManager, MCRCategoryID categID) {
228 MCRCategory categ = categCache.getIfUpToDate(categID, DAO.getLastModified());
229 if (categ != null) {
230 return categ;
231 }
232 categ = MCRCategoryDAOImpl.getByNaturalID(entityManager, categID);
233 if (categ == null) {
234 return null;
235 }
236 categCache.put(categID, categ);
237 return categ;
238 }
239
240 @Override
241 public Map<MCRCategoryID, Boolean> hasLinks(MCRCategory category) {
242 if (category == null) {
243 return hasLinksForClassifications();
244 }
245
246 MCRCategoryImpl rootImpl = (MCRCategoryImpl) MCRCategoryDAOFactory.getInstance()
247 .getCategory(category.getRoot().getId(), -1);
248 if (rootImpl == null) {
249
250 return getNoLinksMap(category);
251 }
252 HashMap<MCRCategoryID, Boolean> boolMap = new HashMap<>();
253 final BitSet linkedInternalIds = getLinkedInternalIds();
254 storeHasLinkValues(boolMap, linkedInternalIds, rootImpl);
255 return boolMap;
256 }
257
258 private Map<MCRCategoryID, Boolean> hasLinksForClassifications() {
259 HashMap<MCRCategoryID, Boolean> boolMap = new HashMap<>() {
260 private static final long serialVersionUID = 1L;
261
262 @Override
263 public Boolean get(Object key) {
264 return Optional.ofNullable(super.get(key)).orElse(Boolean.FALSE);
265 }
266 };
267 final EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
268 TypedQuery<String> linkedClassifications = em.createNamedQuery(NAMED_QUERY_NAMESPACE + "linkedClassifications",
269 String.class);
270 setReadOnly(linkedClassifications);
271 linkedClassifications.getResultList()
272 .stream().map(MCRCategoryID::rootID)
273 .forEach(id -> boolMap.put(id, true));
274 return boolMap;
275 }
276
277 private Map<MCRCategoryID, Boolean> getNoLinksMap(MCRCategory category) {
278 HashMap<MCRCategoryID, Boolean> boolMap = new HashMap<>();
279 for (MCRCategoryID categID : getAllCategIDs(category)) {
280 boolMap.put(categID, false);
281 }
282 return boolMap;
283 }
284
285 private void storeHasLinkValues(HashMap<MCRCategoryID, Boolean> boolMap, BitSet internalIds,
286 MCRCategoryImpl parent) {
287 final int internalID = parent.getInternalID();
288 if (internalID < internalIds.size() && internalIds.get(internalID)) {
289 addParentHasValues(boolMap, parent);
290 } else {
291 boolMap.put(parent.getId(), false);
292 }
293 for (MCRCategory child : parent.getChildren()) {
294 storeHasLinkValues(boolMap, internalIds, (MCRCategoryImpl) child);
295 }
296 }
297
298 private void addParentHasValues(HashMap<MCRCategoryID, Boolean> boolMap, MCRCategory parent) {
299 boolMap.put(parent.getId(), true);
300 if (parent.isCategory() && !Optional.ofNullable(boolMap.get(parent.getParent().getId())).orElse(false)) {
301 addParentHasValues(boolMap, parent.getParent());
302 }
303 }
304
305 private BitSet getLinkedInternalIds() {
306 EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
307 CriteriaBuilder cb = em.getCriteriaBuilder();
308 CriteriaQuery<Number> query = cb.createQuery(Number.class);
309 Root<MCRCategoryLinkImpl> li = query.from(LINK_CLASS);
310 Path<Integer> internalId = li.get(MCRCategoryLinkImpl_.category).get(MCRCategoryImpl_.internalID);
311 List<Number> result = em
312 .createQuery(
313 query.select(internalId)
314 .orderBy(cb.desc(internalId)))
315 .getResultList();
316
317 int maxSize = result.size() == 0 ? 1 : result.get(0).intValue() + 1;
318 BitSet linkSet = new BitSet(maxSize);
319 for (Number internalID : result) {
320 linkSet.set(internalID.intValue(), true);
321 }
322 return linkSet;
323 }
324
325 private static Collection<MCRCategoryID> getAllCategIDs(MCRCategory category) {
326 return MCRStreamUtils.flatten(category, MCRCategory::getChildren, Collection::parallelStream)
327 .map(MCRCategory::getId)
328 .collect(Collectors.toCollection(HashSet::new));
329 }
330
331 private static Collection<MCRCategoryID> getAllChildIDs(MCRCategory category) {
332 return category.getChildren()
333 .stream()
334 .map(MCRCategory::getId)
335 .collect(Collectors.toCollection(HashSet::new));
336 }
337
338 @Override
339 public boolean hasLink(MCRCategory mcrCategory) {
340 return !hasLinks(mcrCategory).isEmpty();
341 }
342
343 @Override
344 public boolean isInCategory(MCRCategLinkReference reference, MCRCategoryID id) {
345 final EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
346 Query q = em.createNamedQuery(NAMED_QUERY_NAMESPACE + "CategoryAndObjectID");
347 setCacheable(q);
348 setReadOnly(q);
349 q.setParameter("rootID", id.getRootID());
350 q.setParameter("categID", id.getID());
351 q.setParameter("objectID", reference.getObjectID());
352 q.setParameter("type", reference.getType());
353 return !q.getResultList().isEmpty();
354 }
355
356 @Override
357 public Collection<MCRCategLinkReference> getReferences(String type) {
358 EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
359 CriteriaBuilder cb = em.getCriteriaBuilder();
360 CriteriaQuery<MCRCategLinkReference> query = cb.createQuery(MCRCategLinkReference.class);
361 Root<MCRCategoryLinkImpl> li = query.from(LINK_CLASS);
362 Path<MCRCategLinkReference> objectReferencePath = li.get(MCRCategoryLinkImpl_.objectReference);
363 return em
364 .createQuery(
365 query.select(objectReferencePath)
366 .where(cb.equal(objectReferencePath.get(MCRCategLinkReference_.type), type)))
367 .setHint(QueryHints.READ_ONLY, "true")
368 .getResultList();
369 }
370
371 @Override
372 public Collection<String> getTypes() {
373 EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
374 TypedQuery<String> q = em.createNamedQuery(NAMED_QUERY_NAMESPACE + "types", String.class);
375 return q.getResultList();
376 }
377
378 @Override
379 public Collection<MCRCategoryLink> getLinks(String type) {
380 EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
381 TypedQuery<MCRCategoryLink> q = em.createNamedQuery(NAMED_QUERY_NAMESPACE + "links", MCRCategoryLink.class);
382 q.setParameter("type", type);
383 return q.getResultList();
384 }
385
386 private static void setReadOnly(Query query) {
387 query.setHint("org.hibernate.readOnly", Boolean.TRUE);
388 }
389
390 private static void setCacheable(Query query) {
391 query.setHint("org.hibernate.cacheable", Boolean.TRUE);
392 }
393
394 }