View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
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   * @author Thomas Scheffler (yagee)
59   *
60   * @version $Revision$ $Date$
61   * @since 2.0
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              // initialize all categIDs with link count of zero
95              countLinks.put(id, 0);
96          }
97          //have to use rootID here if childrenOnly=false
98          //old classification browser/editor could not determine links correctly otherwise
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         // query can take long time, please cache result
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         // get object count for every category (not accumulated)
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         //prepare
145         Collection<String> objectIds = new LinkedList<>();
146         String currentType = ids.iterator().next().getType();
147         typeMap.put(currentType, objectIds);
148         //collect per type
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             //Category does not exist, so it has no links
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 }