001    /**
002     * 
003     * $Revision: 15225 $ $Date: 2009-05-19 15:33:02 +0200 (Tue, 19 May 2009) $
004     *
005     * This file is part of ** M y C o R e **
006     * Visit our homepage at http://www.mycore.de/ for details.
007     *
008     * This program is free software; you can use it, redistribute it
009     * and / or modify it under the terms of the GNU General Public License
010     * (GPL) as published by the Free Software Foundation; either version 2
011     * of the License or (at your option) any later version.
012     *
013     * This program is distributed in the hope that it will be useful, but
014     * WITHOUT ANY WARRANTY; without even the implied warranty of
015     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016     * GNU General Public License for more details.
017     *
018     * You should have received a copy of the GNU General Public License
019     * along with this program, normally in the file license.txt.
020     * If not, write to the Free Software Foundation Inc.,
021     * 59 Temple Place - Suite 330, Boston, MA  02111-1307 USA
022     *
023     **/
024    package org.mycore.datamodel.classifications2.impl;
025    
026    import java.util.ArrayList;
027    import java.util.HashMap;
028    import java.util.HashSet;
029    import java.util.Iterator;
030    import java.util.List;
031    import java.util.Map;
032    
033    import org.apache.log4j.Logger;
034    import org.hibernate.Criteria;
035    import org.hibernate.Query;
036    import org.hibernate.Session;
037    import org.hibernate.criterion.Criterion;
038    import org.hibernate.criterion.DetachedCriteria;
039    import org.hibernate.criterion.Projections;
040    import org.hibernate.criterion.Restrictions;
041    import org.hibernate.criterion.Subqueries;
042    
043    import org.mycore.backend.hibernate.MCRHIBConnection;
044    import org.mycore.common.MCRException;
045    import org.mycore.common.MCRPersistenceException;
046    import org.mycore.datamodel.classifications2.MCRCategory;
047    import org.mycore.datamodel.classifications2.MCRCategoryDAO;
048    import org.mycore.datamodel.classifications2.MCRCategoryID;
049    import org.mycore.datamodel.classifications2.MCRLabel;
050    
051    /**
052     * 
053     * @author Thomas Scheffler (yagee)
054     * 
055     * @version $Revision: 15225 $ $Date: 2008-02-06 17:27:24 +0000 (Mi, 06 Feb
056     *          2008) $
057     * @since 2.0
058     */
059    public class MCRCategoryDAOImpl implements MCRCategoryDAO {
060    
061        private static MCRHIBConnection HIB_CONNECTION_INSTANCE;
062    
063        private static final int LEVEL_START_VALUE = 0;
064    
065        private static final int LEFT_START_VALUE = 0;
066    
067        private static long LAST_MODIFIED = System.currentTimeMillis();
068    
069        private static final Logger LOGGER = Logger.getLogger(MCRCategoryDAOImpl.class);
070    
071        private static final Class<MCRCategoryImpl> CATEGRORY_CLASS = MCRCategoryImpl.class;
072    
073        public void addCategory(MCRCategoryID parentID, MCRCategory category) {
074            if (exist(category.getId())) {
075                throw new MCRException("Cannot add category. A category with ID " + category.getId() + " allready exists");
076            }
077            int leftStart = LEFT_START_VALUE;
078            int levelStart = LEVEL_START_VALUE;
079            Session session = getHibConnection().getSession();
080            MCRCategoryImpl parent = null;
081            if (parentID != null) {
082                parent = getByNaturalID(session, parentID);
083                levelStart = parent.getLevel() + 1;
084                leftStart = parent.getLeft() + 1;
085            }
086            LOGGER.debug("Calculating LEFT,RIGHT and LEVEL attributes...");
087            final MCRCategoryImpl wrapCategory = MCRCategoryImpl.wrapCategory(category, parent, (parent == null) ? category.getRoot() : parent
088                    .getRoot());
089            wrapCategory.calculateLeftRightAndLevel(leftStart, levelStart);
090            // always add +1 for the current node
091            int nodes = 1 + (wrapCategory.getRight() - wrapCategory.getLeft()) / 2;
092            LOGGER.debug("Calculating LEFT,RIGHT and LEVEL attributes. Done! Nodes: " + nodes);
093            if (parentID != null) {
094                final int increment = nodes * 2;
095                updateLeftRightValue(getHibConnection(), parentID.getRootID(), leftStart, increment);
096                parent.getChildren().add(category);
097            }
098            session.save(category);
099            LOGGER.info(new StringBuilder("Category ").append(category.getId()).append(" saved.").toString());
100            updateTimeStamp();
101        }
102    
103        public void deleteCategory(MCRCategoryID id) {
104            final MCRHIBConnection connection = getHibConnection();
105            Session session = connection.getSession();
106            LOGGER.debug("Will get: " + id);
107            MCRCategoryImpl category = getByNaturalID(session, id);
108            if (category == null) {
109                throw new MCRPersistenceException("Category " + id + " was not found. Delete aborted.");
110            }
111            LOGGER.debug("Will delete: " + category.getId());
112            MCRCategory parent = category.parent;
113            category.detachFromParent();
114            session.delete(category);
115            if (parent != null) {
116                LOGGER.debug("Left: " + category.getLeft() + " Right: " + category.getRight());
117                // always add +1 for the currentNode
118                int nodes = 1 + (category.getRight() - category.getLeft()) / 2;
119                final int increment = nodes * -2;
120                // decrement left and right values by nodes
121                updateLeftRightValue(connection, category.getRootID(), category.getLeft(), increment);
122            }
123            updateTimeStamp();
124        }
125    
126        /*
127         * (non-Javadoc)
128         * 
129         * @see org.mycore.datamodel.classifications2.MCRCategoryDAO#exist(org.mycore.datamodel.classifications2.MCRCategoryID)
130         */
131        public boolean exist(MCRCategoryID id) {
132            Criteria criteria = getHibConnection().getSession().createCriteria(CATEGRORY_CLASS);
133            criteria.setProjection(Projections.rowCount()).add(getCategoryCriterion(id));
134            criteria.setCacheable(true);
135            Number result = (Number) criteria.uniqueResult();
136            if (result == null) {
137                return false;
138            }
139            return result.intValue() > 0;
140        }
141    
142        @SuppressWarnings("unchecked")
143        public List<MCRCategory> getCategoriesByLabel(MCRCategoryID baseID, String lang, String text) {
144            Integer[] leftRight = getLeftRightValues(baseID);
145            Query q = getHibConnection().getNamedQuery(CATEGRORY_CLASS.getName() + ".byLabel");
146            q.setString("rootID", baseID.getRootID());
147            q.setInteger("left", leftRight[0]);
148            q.setInteger("right", leftRight[1]);
149            q.setString("lang", lang);
150            q.setString("text", text);
151            q.setCacheable(true);
152            return (List<MCRCategory>) q.list();
153        }
154    
155        @SuppressWarnings("unchecked")
156        public MCRCategory getCategory(MCRCategoryID id, int childLevel) {
157            Session session = getHibConnection().getSession();
158            final boolean fetchAllChildren = childLevel < 0;
159            Query q = null;
160            if (id.isRootID()) {
161                q = getHibConnection().getNamedQuery(
162                        CATEGRORY_CLASS.getName() + (fetchAllChildren ? ".prefetchClassQuery" : ".prefetchClassLevelQuery"));
163                if (!fetchAllChildren)
164                    q.setInteger("endlevel", childLevel);
165                q.setString("classID", id.getRootID());
166            } else {
167                //normal category
168                MCRCategoryImpl category = getByNaturalID(session, id);
169                q = getHibConnection().getNamedQuery(
170                        CATEGRORY_CLASS.getName() + (fetchAllChildren ? ".prefetchCategQuery" : ".prefetchCategLevelQuery"));
171                if (!fetchAllChildren)
172                    q.setInteger("endlevel", category.getLevel() + childLevel);
173                q.setString("classID", id.getRootID());
174                q.setInteger("left", category.getLeft());
175                q.setInteger("right", category.getRight());
176            }
177            List<MCRCategoryImpl> result = q.list();
178            return buildCategoryFromPrefetchedList(result);
179        }
180    
181        /*
182         * (non-Javadoc)
183         * 
184         * @see org.mycore.datamodel.classifications2.MCRClassificationService#getChildren(org.mycore.datamodel.classifications2.MCRCategoryID)
185         */
186        @SuppressWarnings("unchecked")
187        public List<MCRCategory> getChildren(MCRCategoryID cid) {
188            LOGGER.debug("Get children of category: " + cid);
189            if (!exist(cid)) {
190                return new MCRCategoryChildList(null, null);
191            }
192            Session session = getHibConnection().getSession();
193            Criteria c = session.createCriteria(CATEGRORY_CLASS).add(
194                    Subqueries.propertyEq("parent", DetachedCriteria.forClass(CATEGRORY_CLASS)
195                            .setProjection(Projections.property("internalID")).add(getCategoryCriterion(cid))));
196            c.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
197            c.setCacheable(true);
198            return (List<MCRCategory>) c.list();
199        }
200    
201        public List<MCRCategory> getParents(MCRCategoryID id) {
202            // TODO: Make use of left and right value here
203            Session session = getHibConnection().getSession();
204            List<MCRCategory> parents = new ArrayList<MCRCategory>();
205            MCRCategory category = getByNaturalID(session, id);
206            while (category.getParent() != null) {
207                category = category.getParent();
208                parents.add(category);
209            }
210            return parents;
211        }
212    
213        @SuppressWarnings("unchecked")
214        public List<MCRCategoryID> getRootCategoryIDs() {
215            Session session = getHibConnection().getSession();
216            Criteria c = session.createCriteria(CATEGRORY_CLASS);
217            c.add(Restrictions.eq("left", LEFT_START_VALUE));
218            c.setProjection(Projections.projectionList().add(Projections.property("rootID")).add(Projections.property("categID")));
219            c.setCacheable(true);
220            List<Object[]> result = c.list();
221            List<MCRCategoryID> classIds = new ArrayList<MCRCategoryID>(result.size());
222            for (Object[] cat : result) {
223                classIds.add(new MCRCategoryID(cat[0].toString(), cat[1].toString()));
224            }
225            return classIds;
226        }
227    
228        @SuppressWarnings("unchecked")
229        public List<MCRCategory> getRootCategories() {
230            Session session = getHibConnection().getSession();
231            Criteria c = session.createCriteria(CATEGRORY_CLASS);
232            c.add(Restrictions.eq("left", LEFT_START_VALUE));
233            c.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
234            c.setCacheable(true);
235            List<MCRCategoryImpl> result = c.list();
236            List<MCRCategory> classes = new ArrayList<MCRCategory>(result.size());
237            for (MCRCategoryImpl cat : result) {
238                classes.add(copyDeep(cat, 0));
239            }
240            return classes;
241        }
242    
243        public MCRCategory getRootCategory(MCRCategoryID baseID, int childLevel) {
244            if (baseID.isRootID()) {
245                return getCategory(baseID, childLevel);
246            }
247            Session session = getHibConnection().getSession();
248            List<MCRCategory> parents = getParents(baseID);
249            List<MCRCategoryImpl> parentsCopy = new ArrayList<MCRCategoryImpl>(parents.size());
250            for (int i = parents.size() - 1; i >= 0; i--) {
251                parentsCopy.add(copyDeep((MCRCategoryImpl) parents.get(i), 0));
252            }
253            MCRCategoryImpl root = parentsCopy.get(0);
254            for (int i = 1; i < parentsCopy.size(); i++) {
255                parentsCopy.get(i).setRoot(root);
256                parentsCopy.get(i).setParent(parentsCopy.get(i - 1));
257            }
258            MCRCategoryImpl node = getByNaturalID(session, baseID);
259            // prepare a temporary copy for the deepCopy process
260            MCRCategoryImpl tempCopy = new MCRCategoryImpl();
261            tempCopy.setInternalID(node.getInternalID());
262            tempCopy.setId(node.getId());
263            tempCopy.setLabels(node.getLabels());
264            tempCopy.setLevel(node.getLevel());
265            tempCopy.children = node.children;
266            tempCopy.root = root;
267            // attach deep node copy to its parent
268            copyDeep(tempCopy, childLevel).setParent(parentsCopy.get(parentsCopy.size() - 1));
269            // return root node
270            return root;
271        }
272    
273        /*
274         * (non-Javadoc)
275         * 
276         * @see org.mycore.datamodel.classifications2.MCRClassificationService#hasChildren(org.mycore.datamodel.classifications2.MCRCategoryID)
277         */
278        public boolean hasChildren(MCRCategoryID cid) {
279            // SELECT * FROM MCRCATEGORY WHERE PARENTID=(SELECT INTERNALID FROM
280            // MCRCATEGORY WHERE rootID=cid.getRootID() and ID...);
281            return getNumberOfChildren(cid) > 0;
282        }
283    
284        public void moveCategory(MCRCategoryID id, MCRCategoryID newParentID) {
285            int index = getNumberOfChildren(newParentID);
286            moveCategory(id, newParentID, index);
287        }
288    
289        public void moveCategory(MCRCategoryID id, MCRCategoryID newParentID, int index) {
290            final MCRHIBConnection connection = getHibConnection();
291            Session session = connection.getSession();
292            MCRCategoryImpl subTree = getByNaturalID(session, id);
293            int left = subTree.getLeft();
294            int right = subTree.getRight();
295            int oldIndex = subTree.getPositionInParent();
296            MCRCategoryImpl oldParent = (MCRCategoryImpl) subTree.getParent();
297            MCRCategoryImpl newParent = getByNaturalID(session, newParentID);
298            subTree.detachFromParent();
299            LOGGER.debug("Add subtree to new Parent at index: " + index);
300            newParent.getChildren().add(index, subTree);
301            subTree.parent = newParent;
302            // update needed for old and newParent;
303            // Update Left, Right values of other categories
304            boolean movedToRight = isCategoryMovedRight(oldParent, newParent, index, oldIndex);
305            if (LOGGER.isDebugEnabled()) {
306                LOGGER.debug("OldParent left: " + oldParent.getLeft() + " Right: " + oldParent.getRight() + " Level: " + oldParent.getLevel());
307                LOGGER.debug("NewParent left: " + newParent.getLeft() + " Right: " + newParent.getRight() + " Level: " + newParent.getLevel());
308                LOGGER.debug("SubTree   left: " + subTree.getLeft() + " Right: " + subTree.getRight() + " Level: " + subTree.getLevel());
309                if (movedToRight) {
310                    LOGGER.debug("Category '" + id + "' is moved right.");
311                } else {
312                    LOGGER.debug("Category '" + id + "' is moved left.");
313                }
314            }
315            if (movedToRight)
316                updateMoveRight(connection, oldParent, newParent, left, right, index, oldIndex);
317            else
318                updateMoveLeft(connection, oldParent, newParent, left, right, index, oldIndex);
319            // use newParent.left+1 if no left sibling else leftSibling.right+1
320            int leftStart = (index == 0) ? (getLeftRightValues(newParent.getId())[0] + 1) : (getLeftRightValues(newParent.getChildren().get(
321                    index - 1).getId())[1] + 1);
322            // update Left, Right and Level values
323            subTree.calculateLeftRightAndLevel(leftStart, newParent.getLevel() + 1);
324            // only update oldParent if newParent is not its ancestor
325            boolean updateOldParent = (oldParent.getLeft() < newParent.getLeft() || oldParent.getRight() > newParent.getRight()) ? true : false;
326            // only update newParent if newParent!=oldParent and
327            // oldParent is not its ancestor
328            boolean updateNewParent = (!oldParent.getId().equals(newParent.getId()) && (newParent.getLeft() < oldParent.getLeft() || newParent
329                    .getRight() > oldParent.getRight())) ? true : false;
330            if (updateOldParent) {
331                LOGGER.debug("Updating old parent " + oldParent.getId());
332                session.update(oldParent);
333            }
334            if (updateNewParent) {
335                LOGGER.debug("Updating new parent " + newParent.getId());
336                session.update(newParent);
337            }
338            updateTimeStamp();
339        }
340    
341        public void removeLabel(MCRCategoryID id, String lang) {
342            Session session = getHibConnection().getSession();
343            MCRCategoryImpl category = getByNaturalID(session, id);
344            MCRLabel oldLabel = category.getLabel(lang);
345            if (oldLabel != null) {
346                category.getLabels().remove(oldLabel);
347                updateTimeStamp();
348            }
349        }
350    
351        public void replaceCategory(MCRCategory newCategory) throws IllegalArgumentException {
352            if (!exist(newCategory.getId())) {
353                throw new IllegalArgumentException("MCRCategory can not be replaced. MCRCategoryID '" + newCategory.getId() + "' is unknown.");
354            }
355            final MCRHIBConnection connection = getHibConnection();
356            Session session = connection.getSession();
357            MCRCategoryImpl oldCategory = getByNaturalID(session, newCategory.getId());
358            // old Map with all Categories referenced by ID
359            Map<MCRCategoryID, MCRCategoryImpl> oldMap = new HashMap<MCRCategoryID, MCRCategoryImpl>();
360            fillIDMap(oldMap, oldCategory);
361            final MCRCategoryImpl copyDeepImpl = copyDeep(newCategory, -1);
362            MCRCategoryImpl newCategoryImpl = MCRCategoryImpl.wrapCategory(copyDeepImpl, oldCategory.getParent(), oldCategory.getRoot());
363            // new Map with all Categories referenced by ID
364            Map<MCRCategoryID, MCRCategoryID> oldParents = new HashMap<MCRCategoryID, MCRCategoryID>();
365            for (MCRCategory category : oldMap.values()) {
366                if (category.isCategory())
367                    oldParents.put(category.getId(), category.getParent().getId());
368            }
369            Map<MCRCategoryID, MCRCategoryImpl> newMap = new HashMap<MCRCategoryID, MCRCategoryImpl>();
370            fillIDMap(newMap, newCategoryImpl);
371            /*
372             * copy elements from the new tree to the old tree. fixes bug #1848710
373             * (Classification save fails after shifting categories)
374             * 
375             * set internalID for the new rootCategory
376             */
377            session.evict(oldCategory);
378            for (MCRCategoryImpl category : newMap.values()) {
379                MCRCategoryImpl oldValue = oldMap.get(category.getId());
380                if (oldValue != null) {
381                    copyCategoryToNewTree(newCategoryImpl, category, oldValue);
382                }
383            }
384            // calculate left, right and level values
385            int diffNodes = newMap.size() - oldMap.size();
386            LOGGER.debug("Update changes classification node size by: " + diffNodes);
387            int increment = diffNodes * 2;
388            if (increment != 0 && oldCategory.isCategory()) {
389                final int left = oldCategory.getRightSiblingOrOfAncestor().getLeft();
390                final int maxLeft = ((MCRCategoryImpl) oldCategory.getRoot()).getRight();
391                final int right = oldCategory.getRightSiblingOrParent().getRight();
392                final int maxRight = maxLeft;
393                updateLeftRightValueMax(connection, newCategory.getId().getRootID(), left, maxLeft, right, maxRight, increment);
394            }
395            newCategoryImpl.calculateLeftRightAndLevel(oldCategory.getLevel(), oldCategory.getLeft());
396            if (oldCategory.isCategory()) {
397                MCRCategoryImpl parent = (MCRCategoryImpl) oldCategory.getParent();
398                parent.getChildren().set(oldCategory.getPositionInParent(), newCategoryImpl);
399            }
400            // delete removed categories
401            boolean flushBeforeUpdate = false;
402            for (MCRCategoryImpl category : oldMap.values()) {
403                if (!newMap.containsKey(category.getId())) {
404                    LOGGER.info("Deleting category :" + category.getId());
405                    category.detachFromParent();
406                    session.delete(category);
407                    flushBeforeUpdate = true;
408                }
409            }
410            // update shifted categories (JUnit testCase testReplaceCategoryShiftCase)
411            fillIDMap(newMap, newCategoryImpl);
412            for (MCRCategoryImpl category : newMap.values()) {
413                if (category.isCategory() && oldParents.containsKey(category.getId())) {
414                    if (!oldParents.get(category.getId()).equals(category.getParent().getId())) {
415                        LOGGER.info("updating new parent for " + category.getId());
416                        session.update(category);
417                        flushBeforeUpdate = true;
418                    }
419                }
420            }
421            // important to flush here as positionInParent could collide with deleted categories
422            if (flushBeforeUpdate)
423                session.flush();
424            session.saveOrUpdate(newCategoryImpl);
425            updateTimeStamp();
426        }
427    
428        public void setLabel(MCRCategoryID id, MCRLabel label) {
429            Session session = getHibConnection().getSession();
430            MCRCategoryImpl category = getByNaturalID(session, id);
431            MCRLabel oldLabel = category.getLabel(label.getLang());
432            if (oldLabel != null) {
433                category.getLabels().remove(oldLabel);
434            }
435            category.getLabels().add(label);
436            session.update(category);
437            updateTimeStamp();
438        }
439    
440        public long getLastModified() {
441            return LAST_MODIFIED;
442        }
443    
444        private static void updateTimeStamp() {
445            LAST_MODIFIED = System.currentTimeMillis();
446        }
447    
448        /**
449         * return true if a node moved in Category tree is moved to the right.
450         * 
451         * If the <code>newParent</code> is not an ancestor node of
452         * <code>oldParent</code>, a simple comparison of their <code>left</code>
453         * values is done to determine if a node is moved to the right.
454         * 
455         * If the <code>newParent</code> is an ancestor node of
456         * <code>oldParent</code>, the ancestor axis is walked up to the direct
457         * child of <code>newParent</code>. The position of the direct child in
458         * the childrenList of <code>newParent</code> is compared to
459         * <code>newIndex</code> to determine if a node is moved to the right. If
460         * <code>oldParent</code> is an ancestor <code>newParent</code> the
461         * solution is analog.
462         * 
463         * @param oldParent
464         * @param newParent
465         * @param newIndex
466         * @param oldIndex
467         * @return
468         */
469        static boolean isCategoryMovedRight(MCRCategoryImpl oldParent, MCRCategoryImpl newParent, int newIndex, int oldIndex) {
470            int newParentLevel = newParent.getLevel();
471            int oldParentLevel = oldParent.getLevel();
472            if (newParent == oldParent) {
473                LOGGER.debug("oldParent is newParent");
474                return (newIndex > oldIndex);
475            }
476            if (newParentLevel < oldParentLevel
477                    && !((oldParent.getLeft() < newParent.getLeft() || oldParent.getRight() > newParent.getRight()))) {
478                // newParent is ancestor of oldParent
479                MCRCategory node = oldParent;
480                while (!node.getParent().getId().equals(newParent.getId())) {
481                    // walk ancestor axis up while node not direct child of newParent
482                    node = node.getParent();
483                }
484                if (((MCRCategoryImpl) node).getPositionInParent() < newIndex) {
485                    // old ancestor axis is left from new index
486                    return true;
487                }
488                // old ancestor axis is right from new index
489                return false;
490            } else if (newParentLevel > oldParentLevel
491                    && !((newParent.getLeft() < oldParent.getLeft() || newParent.getRight() > oldParent.getRight()))) {
492                MCRCategory node = newParent;
493                while (!node.getParent().getId().equals(oldParent.getId())) {
494                    // walk ancestor axis up while node not direct child of newParent
495                    node = node.getParent();
496                }
497                if (((MCRCategoryImpl) node).getPositionInParent() > oldIndex) {
498                    // new ancestor axis is left from old index
499                    return true;
500                }
501                return false;
502            }
503            /*
504             * oldParent and newParent are on the same level in the tree OR neither
505             * newParent is ancestor of oldParent nor oldParent is ancestor of
506             * newParent.
507             */
508            LOGGER.debug("oldParent and newParent are on the same level in the tree");
509            return (newParent.getLeft() > oldParent.getLeft());
510        }
511    
512        private static Criterion getCategoryCriterion(MCRCategoryID id) {
513            return Restrictions.naturalId().set("rootID", id.getRootID()).set("categID", id.getID());
514        }
515    
516        private static final MCRCategoryImpl buildCategoryFromPrefetchedList(List<MCRCategoryImpl> list) {
517            MCRCategoryImpl baseCat = null;
518            MCRCategoryImpl currentParent = null;
519            for (MCRCategoryImpl currentCat : list) {
520                //make a save copy
521                currentCat = copyDeep(currentCat, 0);
522                if (baseCat == null) {
523                    baseCat = currentCat;
524                    currentParent = currentCat;
525                } else {
526                    //check if currentCat is child of currentParent
527                    if (currentParent.getLevel() + 1 < currentCat.getLevel()) {
528                        currentParent = (MCRCategoryImpl) currentParent.getChildren().get(currentParent.getChildren().size() - 1);
529                    } else
530                        while (currentParent.getLevel() >= currentCat.getLevel()) {
531                            //we have to go some levels up
532                            currentParent = (MCRCategoryImpl) currentParent.getParent();
533                        } //got parent of currentCat as currentParent
534                    currentParent.getChildren().add(currentCat);
535                }
536            }
537            return baseCat;
538        }
539    
540        private static MCRCategoryImpl copyDeep(MCRCategory category, int level) {
541            if (category == null) {
542                return null;
543            }
544            MCRCategoryImpl newCateg = new MCRCategoryImpl();
545            int childAmount;
546            try {
547                childAmount = (level != 0) ? category.getChildren().size() : 0;
548            } catch (RuntimeException e) {
549                LOGGER.error("Cannot get children size for category: " + category.getId(), e);
550                throw e;
551            }
552            newCateg.setChildren(new ArrayList<MCRCategory>(childAmount));
553            newCateg.setId(category.getId());
554            newCateg.setLabels(new HashSet<MCRLabel>(category.getLabels()));
555            newCateg.setRoot(category.getRoot());
556            newCateg.setURI(category.getURI());
557            newCateg.setLevel(category.getLevel());
558            if (category instanceof MCRCategoryImpl) {
559                //to allow optimized hasChildren() to work without db query
560                newCateg.setLeft(((MCRCategoryImpl) category).getLeft());
561                newCateg.setRight(((MCRCategoryImpl) category).getRight());
562                newCateg.setInternalID(((MCRCategoryImpl) category).getInternalID());
563            }
564            if (childAmount > 0) {
565                for (MCRCategory child : category.getChildren()) {
566                    newCateg.getChildren().add(copyDeep((MCRCategoryImpl) child, level - 1));
567                }
568            }
569            return newCateg;
570        }
571    
572        static MCRCategoryImpl getByNaturalID(Session session, MCRCategoryID id) {
573            final Criteria criteria = session.createCriteria(CATEGRORY_CLASS);
574            return (MCRCategoryImpl) criteria.setCacheable(true).add(getCategoryCriterion(id)).uniqueResult();
575        }
576    
577        /**
578         * copies <code>previousVersion</code> into the place of
579         * <code>category</code> in <code>newTree</code>. All changes of
580         * <code>category</code> are applied to <code>previousVersion</code>.
581         * 
582         * @param newTree
583         *            new root of MCRCategory tree
584         * @param category
585         *            new version containing all changes
586         * @param previousVersion
587         *            oldVersion allready stored in database
588         */
589        private static void copyCategoryToNewTree(MCRCategoryImpl newTree, MCRCategoryImpl category, MCRCategoryImpl previousVersion) {
590            category.setInternalID(previousVersion.getInternalID());
591            MCRCategoryImpl parent = (MCRCategoryImpl) category.getParent();
592            if (parent != null) {
593                // get new parent of modified category tree here (by parent category id)
594                parent = (MCRCategoryImpl) findCategory(newTree, category.getParent().getId());
595                int pos = category.getPositionInParent();
596                parent.getChildren().remove(pos);
597                previousVersion.detachFromParent();
598                parent.getChildren().add(pos, previousVersion);
599                // set parent here
600                previousVersion.parent = parent;
601                previousVersion.getChildren().clear();
602                previousVersion.getChildren().addAll(
603                        MCRCategoryImpl.wrapCategories(detachCategories(category.getChildren()), previousVersion, previousVersion.getRoot()));
604                for (MCRLabel newLabel : category.getLabels()) {
605                    MCRLabel oldLabel = previousVersion.getLabel(newLabel.getLang());
606                    if (oldLabel == null) {
607                        // copy new label
608                        previousVersion.getLabels().add(newLabel);
609                    } else {
610                        if (!oldLabel.getText().equals(newLabel.getText())) {
611                            oldLabel.setText(newLabel.getText());
612                        }
613                        if (!oldLabel.getDescription().equals(newLabel.getDescription())) {
614                            oldLabel.setDescription(newLabel.getDescription());
615                        }
616                    }
617                }
618                Iterator<MCRLabel> labels = previousVersion.getLabels().iterator();
619                while (labels.hasNext()) {
620                    // remove labels that are not present in new version
621                    if (category.getLabel(labels.next().getLang()) == null)
622                        labels.remove();
623                }
624                previousVersion.setURI(category.getURI());
625            } else {
626                category.setInternalID(previousVersion.getInternalID());
627            }
628        }
629    
630        /**
631         * finds a MCRCategory with <code>id</code> somewhere below
632         * <code>root</code>.
633         */
634        private static MCRCategory findCategory(MCRCategory root, MCRCategoryID id) {
635            if (root.getId().equals(id)) {
636                return root;
637            }
638            for (MCRCategory child : root.getChildren()) {
639                MCRCategory rv = findCategory(child, id);
640                if (rv != null) {
641                    return rv;
642                }
643            }
644            return null;
645        }
646    
647        /**
648         * detaches all elements in list from their parent.
649         */
650        private static List<MCRCategory> detachCategories(List<MCRCategory> list) {
651            ArrayList<MCRCategory> categories = new ArrayList<MCRCategory>(list.size());
652            categories.addAll(list);
653            for (MCRCategory category : categories) {
654                if (!(category instanceof MCRCategoryImpl)) {
655                    throw new IllegalArgumentException("This method just works for MCRCategoryImpl");
656                }
657                ((MCRAbstractCategoryImpl) category).detachFromParent();
658            }
659            return categories;
660        }
661    
662        private static void fillIDMap(Map<MCRCategoryID, MCRCategoryImpl> map, MCRCategoryImpl category) {
663            map.put(category.getId(), category);
664            for (MCRCategory subCategory : category.getChildren()) {
665                fillIDMap(map, (MCRCategoryImpl) subCategory);
666            }
667        }
668    
669        private static Integer[] getLeftRightValues(MCRCategoryID id) {
670            Session session = getHibConnection().getSession();
671            Criteria c = session.createCriteria(CATEGRORY_CLASS).setProjection(
672                    Projections.projectionList().add(Projections.property("left")).add(Projections.property("right")));
673            c.add(getCategoryCriterion(id));
674            Object[] result = (Object[]) c.uniqueResult();
675            Integer[] iResult = new Integer[] { (Integer) result[0], (Integer) result[1] };
676            return iResult;
677        }
678    
679        private static int getNumberOfChildren(MCRCategoryID id) {
680            Session session = getHibConnection().getSession();
681            Criteria c = session.createCriteria(CATEGRORY_CLASS).setProjection(Projections.rowCount());
682            c.add(Subqueries.propertyEq("parent", DetachedCriteria.forClass(CATEGRORY_CLASS).setProjection(Projections.property("internalID"))
683                    .add(getCategoryCriterion(id))));
684            return ((Number) c.uniqueResult()).intValue();
685        }
686    
687        /**
688         * @param session
689         * @param left
690         * @param increment
691         */
692        private static void updateLeftRightValue(MCRHIBConnection connection, String classID, int left, final int increment) {
693            LOGGER.debug("LEFT AND RIGHT values need updates. Left=" + left + ", increment by: " + increment);
694            Query leftQuery = getHibConnection().getNamedQuery(CATEGRORY_CLASS.getName() + ".updateLeft");
695            leftQuery.setInteger("left", left);
696            leftQuery.setInteger("increment", increment);
697            leftQuery.setString("classID", classID);
698            int leftChanges = leftQuery.executeUpdate();
699            Query rightQuery = getHibConnection().getNamedQuery(CATEGRORY_CLASS.getName() + ".updateRight");
700            rightQuery.setInteger("left", left);
701            rightQuery.setInteger("increment", increment);
702            rightQuery.setString("classID", classID);
703            int rightChanges = rightQuery.executeUpdate();
704            LOGGER.debug("Updated " + leftChanges + " left and " + rightChanges + " right values.");
705        }
706    
707        private static void updateLeftRightValueMax(MCRHIBConnection connection, String classID, int left, int maxLeft, int right,
708                int maxRight, final int increment) {
709            LOGGER.debug("LEFT AND RIGHT values need updates. Left=" + left + ", MaxLeft=" + maxLeft + ", Right=" + right + ", MaxRight="
710                    + maxRight + " increment by: " + increment);
711            Query leftQuery = getHibConnection().getNamedQuery(CATEGRORY_CLASS.getName() + ".updateLeftWithMax");
712            leftQuery.setInteger("left", left);
713            leftQuery.setInteger("max", maxLeft);
714            leftQuery.setInteger("increment", increment);
715            leftQuery.setString("classID", classID);
716            int leftChanges = leftQuery.executeUpdate();
717            Query rightQuery = getHibConnection().getNamedQuery(CATEGRORY_CLASS.getName() + ".updateRightWithMax");
718            rightQuery.setInteger("right", right);
719            rightQuery.setInteger("max", maxRight);
720            rightQuery.setInteger("increment", increment);
721            rightQuery.setString("classID", classID);
722            int rightChanges = rightQuery.executeUpdate();
723            LOGGER.debug("Updated " + leftChanges + " left and " + rightChanges + " right values.");
724        }
725    
726        private static void updateMoveLeft(MCRHIBConnection connection, MCRCategoryImpl oldParent, MCRCategoryImpl newParent, int left,
727                int right, int index, int oldIndex) {
728            int nodes = 1 + (right - left) / 2;
729            int increment = 2 * nodes;
730            MCRCategoryImpl indexedNode = (MCRCategoryImpl) newParent.getChildren().get(index);
731            if (newParent == oldParent) {
732                MCRCategoryImpl leftSiblingOrParent = indexedNode.getLeftSiblingOrParent();
733                int newLeft = leftSiblingOrParent.getLeft();
734                int newRight = (leftSiblingOrParent == newParent) ? newLeft + 1 : leftSiblingOrParent.getRight();
735                updateLeftRightValueMax(connection, newParent.getRootID(), newLeft, left, newRight, right, increment);
736                connection.getSession().refresh(newParent);
737            } else {
738                MCRCategoryImpl rightSiblingOrOfAncestor = indexedNode.getRightSiblingOrOfAncestor();
739                int maxLeft = rightSiblingOrOfAncestor.getLeft();
740                MCRCategoryImpl rightSiblingOrParent = indexedNode.getRightSiblingOrParent();
741                int maxRight = rightSiblingOrParent.getRight();
742                int minRight = right;
743                if (maxRight <= right) {
744                    // Update at least newParent
745                    minRight = maxRight - 1;
746                }
747                updateLeftRightValueMax(connection, newParent.getRootID(), left, maxLeft, minRight, maxRight, increment);
748                connection.getSession().refresh(rightSiblingOrOfAncestor);
749            }
750        }
751    
752        private static void updateMoveRight(MCRHIBConnection connection, MCRCategoryImpl oldParent, MCRCategoryImpl newParent, int left,
753                int right, int index, int oldIndex) {
754            int nodes = 1 + (right - left) / 2;
755            int increment = -2 * nodes;
756            MCRCategoryImpl indexedNode = (MCRCategoryImpl) newParent.getChildren().get(index);
757            if (newParent == oldParent) {
758                MCRCategoryImpl leftSibling = indexedNode.getLeftSiblingOrParent();
759                int maxLeft = leftSibling.getLeft();
760                int maxRight = leftSibling.getRight();
761                updateLeftRightValueMax(connection, newParent.getRootID(), right, maxLeft, left, maxRight, increment);
762                connection.getSession().refresh(newParent);
763            } else {
764                int maxLeft = indexedNode.getLeftSiblingOrParent().getLeft();
765                int minLeft = left;
766                if (maxLeft <= left) {
767                    // update at least newParent
768                    minLeft = maxLeft - 1;
769                }
770                MCRCategoryImpl leftSiblingOrOfAncestor = indexedNode.getLeftSiblingOrOfAncestor();
771                int maxRight = leftSiblingOrOfAncestor.getRight();
772                updateLeftRightValueMax(connection, newParent.getRootID(), minLeft, maxLeft, right, maxRight, increment);
773                connection.getSession().refresh(leftSiblingOrOfAncestor);
774            }
775        }
776    
777        private static MCRHIBConnection getHibConnection() {
778            if (HIB_CONNECTION_INSTANCE == null) {
779                HIB_CONNECTION_INSTANCE = MCRHIBConnection.instance();
780            }
781            return HIB_CONNECTION_INSTANCE;
782        }
783    }