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.net.URI;
22  import java.util.AbstractMap;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.HashMap;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Optional;
29  import java.util.SortedSet;
30  import java.util.function.BiConsumer;
31  import java.util.function.Consumer;
32  import java.util.function.Function;
33  import java.util.stream.Collector;
34  import java.util.stream.Collectors;
35  
36  import org.apache.logging.log4j.LogManager;
37  import org.apache.logging.log4j.Logger;
38  import org.mycore.backend.jpa.MCREntityManagerProvider;
39  import org.mycore.common.MCRException;
40  import org.mycore.common.MCRPersistenceException;
41  import org.mycore.common.MCRStreamUtils;
42  import org.mycore.datamodel.classifications2.MCRCategory;
43  import org.mycore.datamodel.classifications2.MCRCategoryDAO;
44  import org.mycore.datamodel.classifications2.MCRCategoryID;
45  import org.mycore.datamodel.classifications2.MCRLabel;
46  
47  import jakarta.persistence.EntityManager;
48  import jakarta.persistence.EntityNotFoundException;
49  import jakarta.persistence.FlushModeType;
50  import jakarta.persistence.NoResultException;
51  import jakarta.persistence.Query;
52  import jakarta.persistence.TypedQuery;
53  
54  /**
55   * 
56   * @author Thomas Scheffler (yagee)
57   * 
58   * @version $Revision$ $Date: 2008-02-06 17:27:24 +0000 (Mi, 06 Feb
59   *          2008) $
60   * @since 2.0
61   */
62  public class MCRCategoryDAOImpl implements MCRCategoryDAO {
63  
64      private static final int LEVEL_START_VALUE = 0;
65  
66      private static final int LEFT_START_VALUE = 0;
67  
68      private static long LAST_MODIFIED = System.currentTimeMillis();
69  
70      private static final Logger LOGGER = LogManager.getLogger();
71  
72      private static final String NAMED_QUERY_NAMESPACE = "MCRCategory.";
73  
74      private static HashMap<String, Long> LAST_MODIFIED_MAP = new HashMap<>();
75  
76      @Override
77      public MCRCategory addCategory(MCRCategoryID parentID, MCRCategory category) {
78          int position = -1;
79          if (category instanceof MCRCategoryImpl) {
80              position = ((MCRCategoryImpl) category).getPositionInParent();
81          }
82          return addCategory(parentID, category, position);
83      }
84  
85      @Override
86      public MCRCategory addCategory(MCRCategoryID parentID, MCRCategory category, int position) {
87          if (exist(category.getId())) {
88              throw new MCRException("Cannot add category. A category with ID " + category.getId() + " already exists");
89          }
90          return withoutFlush(MCREntityManagerProvider.getCurrentEntityManager(), false, entityManager -> {
91              //we do direct DB manipulation, so flush and clear session first
92              entityManager.flush();
93              entityManager.clear();
94              int leftStart = LEFT_START_VALUE;
95              int levelStart = LEVEL_START_VALUE;
96              MCRCategoryImpl parent = null;
97              if (parentID != null) {
98                  parent = getByNaturalID(entityManager, parentID);
99                  levelStart = parent.getLevel() + 1;
100                 leftStart = parent.getRight();
101                 if (position > parent.getChildren().size()) {
102                     throw new IndexOutOfBoundsException(
103                         "Cannot add category as child #" + position + ", when there are only "
104                             + parent.getChildren().size() + " children.");
105                 }
106             }
107             LOGGER.debug("Calculating LEFT,RIGHT and LEVEL attributes...");
108             final MCRCategoryImpl wrapCategory = MCRCategoryImpl.wrapCategory(category, parent,
109                 parent == null ? category.getRoot() : parent.getRoot());
110             wrapCategory.calculateLeftRightAndLevel(leftStart, levelStart);
111             // always add +1 for the current node
112             int nodes = 1 + (wrapCategory.getRight() - wrapCategory.getLeft()) / 2;
113             LOGGER.debug("Calculating LEFT,RIGHT and LEVEL attributes. Done! Nodes: {}", nodes);
114             if (parentID != null) {
115                 final int increment = nodes * 2;
116                 int parentLeft = parent.getLeft();
117                 updateLeftRightValue(entityManager, parentID.getRootID(), leftStart, increment);
118                 entityManager.flush();
119                 if (position < 0) {
120                     parent.getChildren().add(category);
121                 } else {
122                     parent.getChildren().add(position, category);
123                 }
124                 parent.calculateLeftRightAndLevel(Integer.MAX_VALUE / 2, parent.getLevel());
125                 entityManager.flush();
126                 parent.calculateLeftRightAndLevel(parentLeft, parent.getLevel());
127             }
128             entityManager.persist(category);
129             LOGGER.info("Category {} saved.", category.getId());
130             updateTimeStamp();
131 
132             updateLastModified(category.getRoot().getId().toString());
133             return parent;
134         });
135     }
136 
137     @Override
138     public void deleteCategory(MCRCategoryID id) {
139         EntityManager entityManager = MCREntityManagerProvider.getCurrentEntityManager();
140         LOGGER.debug("Will get: {}", id);
141         MCRCategoryImpl category = getByNaturalID(entityManager, id);
142         try {
143             entityManager.refresh(category); //for MCR-1863
144         } catch (EntityNotFoundException e) {
145             //required since hibernate 5.3 if category is deleted within same transaction.
146             //junit: testLicenses()
147         }
148         if (category == null) {
149             throw new MCRPersistenceException("Category " + id + " was not found. Delete aborted.");
150         }
151         LOGGER.debug("Will delete: {}", category.getId());
152         MCRCategory parent = category.parent;
153         category.detachFromParent();
154         entityManager.remove(category);
155         if (parent != null) {
156             entityManager.flush();
157             LOGGER.debug("Left: {} Right: {}", category.getLeft(), category.getRight());
158             // always add +1 for the currentNode
159             int nodes = 1 + (category.getRight() - category.getLeft()) / 2;
160             final int increment = nodes * -2;
161             // decrement left and right values by nodes
162             updateLeftRightValue(entityManager, category.getRootID(), category.getLeft(), increment);
163         }
164         updateTimeStamp();
165         updateLastModified(category.getRootID());
166     }
167 
168     /*
169      * (non-Javadoc)
170      * 
171      * @see org.mycore.datamodel.classifications2.MCRCategoryDAO#exist(org.mycore.datamodel.classifications2.MCRCategoryID)
172      */
173     @Override
174     public boolean exist(MCRCategoryID id) {
175         return getLeftRightLevelValues(MCREntityManagerProvider.getCurrentEntityManager(), id) != null;
176     }
177 
178     @Override
179     public List<MCRCategory> getCategoriesByLabel(final String lang, final String text) {
180         EntityManager entityManager = MCREntityManagerProvider.getCurrentEntityManager();
181         return cast(entityManager.createNamedQuery(NAMED_QUERY_NAMESPACE + "byLabel", MCRCategoryImpl.class)
182             .setParameter("lang", lang)
183             .setParameter("text", text)
184             .getResultList());
185     }
186 
187     @Override
188     public List<MCRCategory> getCategoriesByLabel(MCRCategoryID baseID, String lang, String text) {
189         EntityManager entityManager = MCREntityManagerProvider.getCurrentEntityManager();
190         MCRCategoryDTO leftRight = getLeftRightLevelValues(entityManager, baseID);
191         return cast(entityManager
192             .createNamedQuery(NAMED_QUERY_NAMESPACE + "byLabelInClass", MCRCategoryImpl.class)
193             .setParameter("rootID", baseID.getRootID())
194             .setParameter("left", leftRight.leftValue)
195             .setParameter("right", leftRight.rightValue)
196             .setParameter("lang", lang)
197             .setParameter("text", text)
198             .getResultList());
199     }
200 
201     @Override
202     @SuppressWarnings("unchecked")
203     public MCRCategory getCategory(MCRCategoryID id, int childLevel) {
204         EntityManager entityManager = MCREntityManagerProvider.getCurrentEntityManager();
205         final boolean fetchAllChildren = childLevel < 0;
206         Query q;
207         if (id.isRootID()) {
208             q = entityManager.createNamedQuery(NAMED_QUERY_NAMESPACE
209                 + (fetchAllChildren ? "prefetchClassQuery" : "prefetchClassLevelQuery"));
210             if (!fetchAllChildren) {
211                 q.setParameter("endlevel", childLevel);
212             }
213             q.setParameter("classID", id.getRootID());
214         } else {
215             //normal category
216             MCRCategoryDTO leftRightLevel = getLeftRightLevelValues(entityManager, id);
217             if (leftRightLevel == null) {
218                 return null;
219             }
220             q = entityManager.createNamedQuery(NAMED_QUERY_NAMESPACE
221                 + (fetchAllChildren ? "prefetchCategQuery" : "prefetchCategLevelQuery"));
222             if (!fetchAllChildren) {
223                 q.setParameter("endlevel", leftRightLevel.level + childLevel);
224             }
225             q.setParameter("classID", id.getRootID());
226             q.setParameter("left", leftRightLevel.leftValue);
227             q.setParameter("right", leftRightLevel.rightValue);
228         }
229         List<MCRCategoryDTO> result = q.getResultList();
230         if (result.isEmpty()) {
231             LOGGER.warn("Could not load category: {}", id);
232             return null;
233         }
234         return buildCategoryFromPrefetchedList(result, id);
235     }
236 
237     /*
238      * (non-Javadoc)
239      * 
240      * @see org.mycore.datamodel.classifications2.MCRClassificationService#getChildren(org.mycore.datamodel.classifications2.MCRCategoryID)
241      */
242     @Override
243     public List<MCRCategory> getChildren(MCRCategoryID cid) {
244         LOGGER.debug("Get children of category: {}", cid);
245         return Optional.ofNullable(cid)
246             .map(id -> getCategory(id, 1))
247             .map(MCRCategory::getChildren)
248             .map(l -> l
249                 .parallelStream()
250                 .collect(Collectors.toList()) //temporary copy for detachFromParent
251                 .parallelStream()
252                 .map(MCRCategoryImpl.class::cast)
253                 .peek(MCRCategoryImpl::detachFromParent)
254                 .map(MCRCategory.class::cast)
255                 .collect(Collectors.toList()))
256             .orElse(new MCRCategoryChildList(null, null));
257     }
258 
259     @Override
260     public List<MCRCategory> getParents(MCRCategoryID id) {
261         EntityManager entityManager = MCREntityManagerProvider.getCurrentEntityManager();
262         MCRCategoryDTO leftRight = getLeftRightLevelValues(entityManager, id);
263         if (leftRight == null) {
264             return null;
265         }
266         Query parentQuery = entityManager
267             .createNamedQuery(NAMED_QUERY_NAMESPACE + "parentQuery")
268             .setParameter("classID", id.getRootID())
269             .setParameter("categID", id.getID())
270             .setParameter("left", leftRight.leftValue)
271             .setParameter("right", leftRight.rightValue);
272         @SuppressWarnings("unchecked")
273         List<MCRCategoryDTO> resultList = parentQuery.getResultList();
274         MCRCategory category = buildCategoryFromPrefetchedList(resultList, id);
275         List<MCRCategory> parents = new ArrayList<>();
276         while (category.getParent() != null) {
277             category = category.getParent();
278             parents.add(category);
279         }
280         return parents;
281     }
282 
283     @Override
284     @SuppressWarnings("unchecked")
285     public List<MCRCategoryID> getRootCategoryIDs() {
286         EntityManager entityManager = MCREntityManagerProvider.getCurrentEntityManager();
287         return entityManager.createNamedQuery(NAMED_QUERY_NAMESPACE + "rootIds").getResultList();
288     }
289 
290     @Override
291     @SuppressWarnings("unchecked")
292     public List<MCRCategory> getRootCategories() {
293         EntityManager entityManager = MCREntityManagerProvider.getCurrentEntityManager();
294         List<MCRCategoryDTO> resultList = entityManager.createNamedQuery(NAMED_QUERY_NAMESPACE + "rootCategs")
295             .getResultList();
296         BiConsumer<List<MCRCategory>, MCRCategoryImpl> merge = (l, c) -> {
297             MCRCategoryImpl last = (MCRCategoryImpl) l.get(l.size() - 1);
298             if (last.getInternalID() != c.getInternalID()) {
299                 l.add(c);
300             } else {
301                 last.getLabels().addAll(c.getLabels());
302             }
303         };
304         return resultList.parallelStream()
305             .map(c -> c.merge(null))
306             .collect(Collector.of(ArrayList::new,
307                 (ArrayList<MCRCategory> l, MCRCategoryImpl c) -> {
308                     if (l.isEmpty()) {
309                         l.add(c);
310                     } else {
311                         merge.accept(l, c);
312                     }
313                 }, (l, r) -> {
314                     if (l.isEmpty()) {
315                         return r;
316                     }
317                     if (r.isEmpty()) {
318                         return l;
319                     }
320                     MCRCategoryImpl first = (MCRCategoryImpl) r.get(0);
321                     merge.accept(l, first);
322                     l.addAll(r.subList(1, r.size()));
323                     return l;
324                 }));
325     }
326 
327     @Override
328     public MCRCategory getRootCategory(MCRCategoryID baseID, int childLevel) {
329         return Optional.ofNullable(getCategory(baseID, childLevel))
330             .map(c -> {
331                 if (baseID.isRootID()) {
332                     return c;
333                 }
334                 List<MCRCategory> parents = getParents(baseID);
335                 MCRCategory parent = parents.get(0);
336                 c.getChildren()
337                     .stream()
338                     .collect(Collectors.toList())
339                     .stream()
340                     .map(MCRCategoryImpl.class::cast)
341                     .peek(MCRCategoryImpl::detachFromParent)
342                     .forEachOrdered(parent.getChildren()::add);
343                 // return root node
344                 return parents.get(parents.size() - 1);
345             })
346             .orElse(null);
347     }
348 
349     /*
350      * (non-Javadoc)
351      * 
352      * @see org.mycore.datamodel.classifications2.MCRClassificationService#hasChildren(org.mycore.datamodel.classifications2.MCRCategoryID)
353      */
354     @Override
355     public boolean hasChildren(MCRCategoryID cid) {
356         // SELECT * FROM MCRCATEGORY WHERE PARENTID=(SELECT INTERNALID FROM
357         // MCRCATEGORY WHERE rootID=cid.getRootID() and ID...);
358         return getNumberOfChildren(MCREntityManagerProvider.getCurrentEntityManager(), cid) > 0;
359     }
360 
361     @Override
362     public void moveCategory(MCRCategoryID id, MCRCategoryID newParentID) {
363         int index = getNumberOfChildren(MCREntityManagerProvider.getCurrentEntityManager(), newParentID);
364         moveCategory(id, newParentID, index);
365     }
366 
367     private MCRCategoryImpl getCommonAncestor(EntityManager entityManager, MCRCategoryImpl node1,
368         MCRCategoryImpl node2) {
369         if (!node1.getRootID().equals(node2.getRootID())) {
370             return null;
371         }
372         if (node1.getLeft() == 0) {
373             return node1;
374         }
375         if (node2.getLeft() == 0) {
376             return node2;
377         }
378         int left = Math.min(node1.getLeft(), node2.getLeft());
379         int right = Math.max(node1.getRight(), node2.getRight());
380         Query q = entityManager.createNamedQuery(NAMED_QUERY_NAMESPACE + "commonAncestor")
381             .setMaxResults(1)
382             .setParameter("left", left)
383             .setParameter("right", right)
384             .setParameter("rootID", node1.getRootID());
385         return getSingleResult(q);
386     }
387 
388     @Override
389     public void moveCategory(MCRCategoryID id, MCRCategoryID newParentID, int index) {
390         withoutFlush(MCREntityManagerProvider.getCurrentEntityManager(), true, e -> {
391             MCRCategoryImpl subTree = getByNaturalID(MCREntityManagerProvider.getCurrentEntityManager(), id);
392             MCRCategoryImpl oldParent = (MCRCategoryImpl) subTree.getParent();
393             MCRCategoryImpl newParent = getByNaturalID(MCREntityManagerProvider.getCurrentEntityManager(), newParentID);
394             MCRCategoryImpl commonAncestor = getCommonAncestor(MCREntityManagerProvider.getCurrentEntityManager(),
395                 oldParent, newParent);
396             subTree.detachFromParent();
397             LOGGER.debug("Add subtree to new Parent at index: {}", index);
398             newParent.getChildren().add(index, subTree);
399             subTree.parent = newParent;
400             MCREntityManagerProvider.getCurrentEntityManager().flush();
401             int left = commonAncestor.getLeft();
402             commonAncestor.calculateLeftRightAndLevel(Integer.MAX_VALUE / 2, commonAncestor.getLevel());
403             e.flush();
404             commonAncestor.calculateLeftRightAndLevel(left, commonAncestor.getLevel());
405             updateTimeStamp();
406             updateLastModified(id.getRootID());
407         });
408     }
409 
410     @Override
411     public MCRCategory removeLabel(MCRCategoryID id, String lang) {
412         EntityManager entityManager = MCREntityManagerProvider.getCurrentEntityManager();
413         MCRCategoryImpl category = getByNaturalID(entityManager, id);
414         category.getLabel(lang).ifPresent(oldLabel -> {
415             category.getLabels().remove(oldLabel);
416             updateTimeStamp();
417             updateLastModified(category.getRootID());
418         });
419         return category;
420     }
421 
422     @Override
423     public Collection<MCRCategoryImpl> replaceCategory(MCRCategory newCategory) throws IllegalArgumentException {
424         if (!exist(newCategory.getId())) {
425             throw new IllegalArgumentException(
426                 "MCRCategory can not be replaced. MCRCategoryID '" + newCategory.getId() + "' is unknown.");
427         }
428         return withoutFlush(MCREntityManagerProvider.getCurrentEntityManager(), true, em -> {
429             MCRCategoryImpl oldCategory = getByNaturalID(MCREntityManagerProvider.getCurrentEntityManager(),
430                 newCategory.getId());
431             int oldLevel = oldCategory.getLevel();
432             int oldLeft = oldCategory.getLeft();
433             // old Map with all Categories referenced by ID
434             Map<MCRCategoryID, MCRCategoryImpl> oldMap = toMap(oldCategory);
435             final MCRCategoryImpl copyDeepImpl = copyDeep(newCategory, -1);
436             MCRCategoryImpl newCategoryImpl = MCRCategoryImpl.wrapCategory(copyDeepImpl, oldCategory.getParent(),
437                 oldCategory.getRoot());
438             // new Map with all Categories referenced by ID
439             Map<MCRCategoryID, MCRCategoryImpl> newMap = toMap(newCategoryImpl);
440             //remove;
441             oldMap
442                 .entrySet()
443                 .stream()
444                 .filter(c -> !newMap.containsKey(c.getKey()))
445                 .map(Map.Entry::getValue)
446                 .peek(MCRCategoryDAOImpl::remove)
447                 .forEach(c -> LOGGER.info("remove category: {}", c.getId()));
448             oldMap.clear();
449             oldMap.putAll(toMap(oldCategory));
450             //sync labels/uris;
451             MCRStreamUtils
452                 .flatten(oldCategory, MCRCategory::getChildren, Collection::stream)
453                 .filter(c -> newMap.containsKey(c.getId()))
454                 .map(MCRCategoryImpl.class::cast)
455                 .map(c -> new AbstractMap.SimpleEntry<>(c, newMap.get(c.getId())))
456                 // key: category of old version, value: category of new version
457                 .peek(e -> syncLabels(e.getValue(), e.getKey())) //sync from new to old version
458                 .forEach(e -> e.getKey().setURI(e.getValue().getURI()));
459             //detach all categories, we will rebuild tree structure later
460             oldMap
461                 .values()
462                 .stream()
463                 .filter(c -> c.getInternalID() != oldCategory.getInternalID()) //do not detach root of subtree
464                 .forEach(MCRCategoryImpl::detachFromParent);
465             //rebuild
466             MCRStreamUtils
467                 .flatten(newCategoryImpl, MCRCategory::getChildren, Collection::stream)
468                 .forEachOrdered(c -> {
469                     MCRCategoryImpl oldC = oldMap.get(c.getId());
470                     oldC.setChildren(
471                         c
472                             .getChildren()
473                             .stream()
474                             .map(cc -> {
475                                 //to categories of stored version or copy from new version
476                                 MCRCategoryImpl oldCC = oldMap.get(cc.getId());
477                                 if (oldCC == null) {
478                                     oldCC = new MCRCategoryImpl();
479                                     oldCC.setId(cc.getId());
480                                     oldCC.setURI(cc.getURI());
481                                     oldCC.getLabels().addAll(cc.getLabels());
482                                     oldMap.put(oldCC.getId(), oldCC);
483                                 }
484                                 return oldCC;
485                             })
486                             .collect(Collectors.toList()));
487                 });
488             oldCategory.calculateLeftRightAndLevel(Integer.MAX_VALUE / 2, oldLevel);
489             em.flush();
490             oldCategory.calculateLeftRightAndLevel(oldLeft, oldLevel);
491             updateTimeStamp();
492             updateLastModified(newCategory.getId().getRootID());
493             return newMap.values();
494         });
495     }
496 
497     private static Map<MCRCategoryID, MCRCategoryImpl> toMap(MCRCategoryImpl oldCategory) {
498         return MCRStreamUtils
499             .flatten(oldCategory, MCRCategory::getChildren, Collection::stream)
500             .collect(Collectors.toMap(MCRCategory::getId, MCRCategoryImpl.class::cast));
501     }
502 
503     private static void remove(MCRCategoryImpl category) {
504         if (category.hasChildren()) {
505             int parentPos = category.getPositionInParent();
506             MCRCategoryImpl parent = (MCRCategoryImpl) category.getParent();
507             @SuppressWarnings("unchecked")
508             ArrayList<MCRCategoryImpl> copy = new ArrayList(category.children);
509             copy.forEach(MCRCategoryImpl::detachFromParent);
510             parent.children.addAll(parentPos, copy);
511             copy.forEach(c -> c.parent = parent); //fixes MCR-1963
512         }
513         category.detachFromParent();
514         MCREntityManagerProvider.getCurrentEntityManager().remove(category);
515     }
516 
517     @Override
518     public MCRCategory setLabel(MCRCategoryID id, MCRLabel label) {
519         EntityManager entityManager = MCREntityManagerProvider.getCurrentEntityManager();
520         MCRCategoryImpl category = getByNaturalID(entityManager, id);
521         category.getLabel(label.getLang()).ifPresent(category.getLabels()::remove);
522         category.getLabels().add(label);
523         updateTimeStamp();
524         updateLastModified(category.getRootID());
525         return category;
526     }
527 
528     @Override
529     public MCRCategory setLabels(MCRCategoryID id, SortedSet<MCRLabel> labels) {
530         EntityManager entityManager = MCREntityManagerProvider.getCurrentEntityManager();
531         MCRCategoryImpl category = getByNaturalID(entityManager, id);
532         category.setLabels(labels);
533         updateTimeStamp();
534         updateLastModified(category.getRootID());
535         return category;
536     }
537 
538     @Override
539     public MCRCategory setURI(MCRCategoryID id, URI uri) {
540         EntityManager entityManager = MCREntityManagerProvider.getCurrentEntityManager();
541         MCRCategoryImpl category = getByNaturalID(entityManager, id);
542         category.setURI(uri);
543         updateTimeStamp();
544         updateLastModified(category.getRootID());
545         return category;
546     }
547 
548     public void repairLeftRightValue(String classID) {
549         final MCRCategoryID rootID = MCRCategoryID.rootID(classID);
550         withoutFlush(MCREntityManagerProvider.getCurrentEntityManager(), true, entityManager -> {
551             MCRCategoryImpl classification = MCRCategoryDAOImpl.getByNaturalID(entityManager, rootID);
552             classification.calculateLeftRightAndLevel(Integer.MAX_VALUE / 2, LEVEL_START_VALUE);
553             entityManager.flush();
554             classification.calculateLeftRightAndLevel(LEFT_START_VALUE, LEVEL_START_VALUE);
555         });
556     }
557 
558     @Override
559     public long getLastModified() {
560         return LAST_MODIFIED;
561     }
562 
563     private static void updateTimeStamp() {
564         LAST_MODIFIED = System.currentTimeMillis();
565     }
566 
567     private static MCRCategoryImpl buildCategoryFromPrefetchedList(List<MCRCategoryDTO> list, MCRCategoryID returnID) {
568         LOGGER.debug(() -> "using prefetched list: " + list);
569         MCRCategoryImpl predecessor = null;
570         for (MCRCategoryDTO entry : list) {
571             predecessor = entry.merge(predecessor);
572         }
573         return MCRStreamUtils.flatten(predecessor.getRoot(), MCRCategory::getChildren, Collection::parallelStream)
574             .filter(c -> c.getId().equals(returnID))
575             .findFirst()
576             .map(MCRCategoryImpl.class::cast)
577             .orElseThrow(() -> new MCRException("Could not find " + returnID + " in database result."));
578     }
579 
580     private static MCRCategoryImpl copyDeep(MCRCategory category, int level) {
581         if (category == null) {
582             return null;
583         }
584         MCRCategoryImpl newCateg = new MCRCategoryImpl();
585         int childAmount;
586         try {
587             childAmount = level != 0 ? category.getChildren().size() : 0;
588         } catch (RuntimeException e) {
589             LOGGER.error("Cannot get children size for category: {}", category.getId(), e);
590             throw e;
591         }
592         newCateg.setChildren(new ArrayList<>(childAmount));
593         newCateg.setId(category.getId());
594         newCateg.setLabels(category.getLabels());
595         newCateg.setRoot(category.getRoot());
596         newCateg.setURI(category.getURI());
597         newCateg.setLevel(category.getLevel());
598         if (category instanceof MCRCategoryImpl) {
599             //to allow optimized hasChildren() to work without db query
600             newCateg.setLeft(((MCRCategoryImpl) category).getLeft());
601             newCateg.setRight(((MCRCategoryImpl) category).getRight());
602             newCateg.setInternalID(((MCRCategoryImpl) category).getInternalID());
603         }
604         if (childAmount > 0) {
605             for (MCRCategory child : category.getChildren()) {
606                 newCateg.getChildren().add(copyDeep(child, level - 1));
607             }
608         }
609         return newCateg;
610     }
611 
612     /**
613      * returns database backed MCRCategoryImpl
614      * 
615      * every change to the returned MCRCategory is reflected in the database.
616      */
617     public static MCRCategoryImpl getByNaturalID(EntityManager entityManager, MCRCategoryID id) {
618         TypedQuery<MCRCategoryImpl> naturalIDQuery = entityManager
619             .createNamedQuery(NAMED_QUERY_NAMESPACE + "byNaturalId", MCRCategoryImpl.class)
620             .setParameter("classID", id.getRootID())
621             .setParameter("categID", id.getID());
622         return getSingleResult(naturalIDQuery);
623     }
624 
625     private static void syncLabels(MCRCategoryImpl source, MCRCategoryImpl target) {
626         for (MCRLabel newLabel : source.getLabels()) {
627             Optional<MCRLabel> label = target.getLabel(newLabel.getLang());
628             if (!label.isPresent()) {
629                 // copy new label
630                 target.getLabels().add(newLabel);
631             }
632             label.ifPresent(oldLabel -> {
633                 if (!oldLabel.getText().equals(newLabel.getText())) {
634                     oldLabel.setText(newLabel.getText());
635                 }
636                 if (!oldLabel.getDescription().equals(newLabel.getDescription())) {
637                     oldLabel.setDescription(newLabel.getDescription());
638                 }
639             });
640         }
641         // remove labels that are not present in new version
642         target.getLabels().removeIf(mcrLabel -> !source.getLabel(mcrLabel.getLang()).isPresent());
643     }
644 
645     private static MCRCategoryDTO getLeftRightLevelValues(EntityManager entityManager, MCRCategoryID id) {
646         return getSingleResult(entityManager
647             .createNamedQuery(NAMED_QUERY_NAMESPACE + "leftRightLevelQuery")
648             .setParameter("categID", id));
649     }
650 
651     private static int getNumberOfChildren(EntityManager entityManager, MCRCategoryID id) {
652         return getSingleResult(entityManager
653             .createNamedQuery(NAMED_QUERY_NAMESPACE + "childCount")
654             .setParameter("classID", id.getRootID())
655             .setParameter("categID", id.getID()));
656     }
657 
658     private static void updateLeftRightValue(EntityManager entityManager, String classID, int left,
659         final int increment) {
660         withoutFlush(entityManager, true, e -> {
661             LOGGER.debug("LEFT AND RIGHT values need updates. Left={}, increment by: {}", left, increment);
662             Query leftQuery = e
663                 .createNamedQuery(NAMED_QUERY_NAMESPACE + "updateLeft")
664                 .setParameter("left", left)
665                 .setParameter("increment", increment)
666                 .setParameter("classID", classID);
667             int leftChanges = leftQuery.executeUpdate();
668             Query rightQuery = e
669                 .createNamedQuery(NAMED_QUERY_NAMESPACE + "updateRight")
670                 .setParameter("left", left)
671                 .setParameter("increment", increment)
672                 .setParameter("classID", classID);
673             int rightChanges = rightQuery.executeUpdate();
674             LOGGER.debug("Updated {} left and {} right values.", leftChanges, rightChanges);
675         });
676     }
677 
678     /**
679      * Method updates the last modified timestamp, for the given root id.
680      * 
681      */
682     protected synchronized void updateLastModified(String root) {
683         LAST_MODIFIED_MAP.put(root, System.currentTimeMillis());
684     }
685 
686     /**
687      * Gets the timestamp for the given root id. If there is not timestamp at the moment -1 is returned.
688      * 
689      * @return the last modified timestamp (if any) or -1
690      */
691     @Override
692     public long getLastModified(String root) {
693         Long long1 = LAST_MODIFIED_MAP.get(root);
694         if (long1 != null) {
695             return long1;
696         }
697         return -1;
698     }
699 
700     @SuppressWarnings("unchecked")
701     private static <T> T getSingleResult(Query query) {
702         try {
703             return (T) query.getSingleResult();
704         } catch (NoResultException e) {
705             return null;
706         }
707     }
708 
709     private static List<MCRCategory> cast(List<MCRCategoryImpl> list) {
710         @SuppressWarnings({ "unchecked", "rawtypes" })
711         List<MCRCategory> temp = (List) list;
712         return temp;
713     }
714 
715     private static <T> T withoutFlush(EntityManager entityManager, boolean flushAtEnd,
716         Function<EntityManager, T> task) {
717         FlushModeType fm = entityManager.getFlushMode();
718         entityManager.setFlushMode(FlushModeType.COMMIT);
719         try {
720             T result = task.apply(entityManager);
721             if (flushAtEnd) {
722                 entityManager.flush();
723             }
724             return result;
725         } catch (RuntimeException e) {
726             throw e;
727         } finally {
728             entityManager.setFlushMode(fm);
729         }
730     }
731 
732     private static void withoutFlush(EntityManager entityManager, boolean flushAtEnd, Consumer<EntityManager> task) {
733         withoutFlush(entityManager, flushAtEnd, e -> {
734             task.accept(e);
735             return null;
736         });
737     }
738 }