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.io.Serializable;
22  import java.net.URI;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.List;
26  import java.util.SortedSet;
27  import java.util.stream.Collectors;
28  
29  import org.apache.logging.log4j.LogManager;
30  import org.apache.logging.log4j.Logger;
31  import org.hibernate.annotations.SortNatural;
32  import org.mycore.backend.jpa.MCRURIConverter;
33  import org.mycore.common.MCRException;
34  import org.mycore.datamodel.classifications2.MCRCategory;
35  import org.mycore.datamodel.classifications2.MCRCategoryID;
36  import org.mycore.datamodel.classifications2.MCRLabel;
37  
38  import jakarta.persistence.Access;
39  import jakarta.persistence.AccessType;
40  import jakarta.persistence.CascadeType;
41  import jakarta.persistence.CollectionTable;
42  import jakarta.persistence.Column;
43  import jakarta.persistence.Convert;
44  import jakarta.persistence.ElementCollection;
45  import jakarta.persistence.Embedded;
46  import jakarta.persistence.Entity;
47  import jakarta.persistence.FetchType;
48  import jakarta.persistence.GeneratedValue;
49  import jakarta.persistence.GenerationType;
50  import jakarta.persistence.Id;
51  import jakarta.persistence.Index;
52  import jakarta.persistence.JoinColumn;
53  import jakarta.persistence.ManyToOne;
54  import jakarta.persistence.NamedQueries;
55  import jakarta.persistence.NamedQuery;
56  import jakarta.persistence.OneToMany;
57  import jakarta.persistence.OrderColumn;
58  import jakarta.persistence.Table;
59  import jakarta.persistence.Transient;
60  import jakarta.persistence.UniqueConstraint;
61  
62  /**
63   * 
64   * @author Thomas Scheffler (yagee)
65   * 
66   * @version $Revision$ $Date$
67   * @since 2.0
68   */
69  @Entity
70  @Table(name = "MCRCategory",
71      indexes = {
72          @Index(columnList = "ClassID, leftValue, rightValue", name = "ClassLeftRight"),
73          @Index(columnList = "leftValue", name = "ClassesRoot")
74      },
75      uniqueConstraints = {
76          @UniqueConstraint(columnNames = { "ClassID", "CategID" }, name = "ClassCategUnique"),
77          @UniqueConstraint(columnNames = { "ClassID", "leftValue" }, name = "ClassLeftUnique"),
78          @UniqueConstraint(columnNames = { "ClassID", "rightValue" }, name = "ClassRightUnique") })
79  @NamedQueries({
80      @NamedQuery(name = "MCRCategory.updateLeft",
81          query = "UPDATE MCRCategoryImpl cat SET cat.left=cat.left+:increment WHERE "
82              + "cat.id.rootID= :classID AND cat.left >= :left"),
83      @NamedQuery(name = "MCRCategory.updateRight",
84          query = "UPDATE MCRCategoryImpl cat SET cat.right=cat.right+:increment WHERE "
85              + "cat.id.rootID= :classID AND cat.right >= :left"),
86      @NamedQuery(name = "MCRCategory.commonAncestor",
87          query = "FROM MCRCategoryImpl as cat WHERE "
88              + "cat.id.rootID=:rootID AND cat.left < :left AND cat.right > :right "
89              + "ORDER BY cat.left DESC"),
90      @NamedQuery(name = "MCRCategory.byNaturalId",
91          query = "FROM MCRCategoryImpl as cat WHERE "
92              + "cat.id.rootID=:classID and (cat.id.id=:categID OR cat.id.id IS NULL AND :categID IS NULL)"),
93      @NamedQuery(name = "MCRCategory.byLabelInClass",
94          query = "FROM MCRCategoryImpl as cat "
95              + "INNER JOIN cat.labels as label "
96              + "  WHERE cat.id.rootID=:rootID AND "
97              + "    cat.left BETWEEN :left and :right AND "
98              + "    label.lang=:lang AND "
99              + "    label.text=:text"),
100     @NamedQuery(name = "MCRCategory.byLabel",
101         query = "FROM MCRCategoryImpl as cat "
102             + "  INNER JOIN cat.labels as label "
103             + "  WHERE label.lang=:lang AND "
104             + "    label.text=:text"),
105     @NamedQuery(name = "MCRCategory.prefetchClassQuery",
106         query = MCRCategoryDTO.SELECT
107             + " WHERE cat.id.rootID=:classID ORDER BY cat.left"),
108     @NamedQuery(name = "MCRCategory.prefetchClassLevelQuery",
109         query = MCRCategoryDTO.SELECT
110             + " WHERE cat.id.rootID=:classID AND cat.level <= :endlevel ORDER BY cat.left"),
111     @NamedQuery(name = "MCRCategory.prefetchCategQuery",
112         query = MCRCategoryDTO.SELECT
113             + " WHERE cat.id.rootID=:classID AND (cat.left BETWEEN :left AND :right OR cat.left=0) ORDER BY cat.left"),
114     @NamedQuery(name = "MCRCategory.prefetchCategLevelQuery",
115         query = MCRCategoryDTO.SELECT
116             + " WHERE cat.id.rootID=:classID AND (cat.left BETWEEN :left AND :right OR cat.left=0) AND "
117             + "cat.level <= :endlevel ORDER BY cat.left"),
118     @NamedQuery(name = "MCRCategory.leftRightLevelQuery",
119         query = MCRCategoryDTO.LRL_SELECT
120             + " WHERE cat.id=:categID "),
121     @NamedQuery(name = "MCRCategory.parentQuery",
122         query = MCRCategoryDTO.SELECT
123             + " WHERE cat.id.rootID=:classID AND (cat.left < :left AND cat.right > :right OR cat.id.id=:categID) "
124             + "ORDER BY cat.left"),
125     @NamedQuery(name = "MCRCategory.rootCategs",
126         query = MCRCategoryDTO.SELECT
127             + " WHERE cat.left = 0 ORDER BY cat.id.rootID"),
128     @NamedQuery(name = "MCRCategory.rootIds", query = "SELECT cat.id FROM MCRCategoryImpl cat WHERE cat.left = 0"),
129     @NamedQuery(name = "MCRCategory.childCount",
130         query = "SELECT CAST(count(*) AS integer) FROM MCRCategoryImpl children WHERE "
131             + "children.parent=(SELECT cat.internalID FROM MCRCategoryImpl cat WHERE "
132             + "cat.id.rootID=:classID and (cat.id.id=:categID OR cat.id.id IS NULL AND :categID IS NULL))"),
133 })
134 
135 @Access(AccessType.PROPERTY)
136 public class MCRCategoryImpl extends MCRAbstractCategoryImpl implements Serializable {
137 
138     private static final long serialVersionUID = -7431317191711000317L;
139 
140     private static Logger LOGGER = LogManager.getLogger(MCRCategoryImpl.class);
141 
142     private int left, right, internalID;
143 
144     int level;
145 
146     public MCRCategoryImpl() {
147     }
148 
149     //Mapping definition
150 
151     @Id
152     @GeneratedValue(strategy = GenerationType.IDENTITY)
153     public int getInternalID() {
154         return internalID;
155     }
156 
157     @Column(name = "leftValue")
158     public int getLeft() {
159         return left;
160     }
161 
162     @Column(name = "rightValue")
163     public int getRight() {
164         return right;
165     }
166 
167     @Column
168     public int getLevel() {
169         return level;
170     }
171 
172     @Transient
173     int getPositionInParent() {
174         LOGGER.debug("getposition called for {}", getId());
175         if (parent == null) {
176             LOGGER.debug("getposition called with no parent set.");
177             return -1;
178         }
179         try {
180             int position = getParent().getChildren().indexOf(this);
181             if (position == -1) {
182                 // sometimes indexOf does not find this instance so we need to
183                 // check for the ID here to
184                 position = getPositionInParentByID();
185             }
186             return position;
187         } catch (RuntimeException e) {
188             LOGGER.error("Cannot use parent.getChildren() here", e);
189             throw e;
190         }
191     }
192 
193     //    @NaturalId
194     @Override
195     @Embedded
196     public MCRCategoryID getId() {
197         return super.getId();
198     }
199 
200     @Override
201     @OneToMany(targetEntity = MCRCategoryImpl.class,
202         cascade = {
203             CascadeType.ALL },
204         mappedBy = "parent")
205     @OrderColumn(name = "positionInParent")
206     @Access(AccessType.FIELD)
207     public List<MCRCategory> getChildren() {
208         return super.getChildren();
209     }
210 
211     @Override
212     @ElementCollection(fetch = FetchType.LAZY)
213     @CollectionTable(name = "MCRCategoryLabels",
214         joinColumns = @JoinColumn(name = "category"),
215         uniqueConstraints = {
216             @UniqueConstraint(columnNames = { "category", "lang" }) })
217     @SortNatural
218     public SortedSet<MCRLabel> getLabels() {
219         return super.getLabels();
220     }
221 
222     @Override
223     @Column
224     @Convert(converter = MCRURIConverter.class)
225     public URI getURI() {
226         return super.getURI();
227     }
228 
229     @ManyToOne(optional = true, targetEntity = MCRCategoryImpl.class)
230     @JoinColumn(name = "parentID")
231     @Access(AccessType.FIELD)
232     public MCRCategory getParent() {
233         return super.getParent();
234     }
235 
236     //End of Mapping
237 
238     @Override
239     public boolean hasChildren() {
240         //if children is initialized and has objects use it and don't depend on db values
241         if (children != null && children.size() > 0) {
242             return true;
243         }
244         if (right != left) {
245             return right - left > 1;
246         }
247         return super.hasChildren();
248     }
249 
250     @Transient
251     private int getPositionInParentByID() {
252         int position = 0;
253         for (MCRCategory sibling : parent.getChildren()) {
254             if (getId().equals(sibling.getId())) {
255                 return position;
256             }
257             position++;
258         }
259         if (LOGGER.isDebugEnabled()) {
260             StringBuilder sb = new StringBuilder("List of children of parent: ");
261             sb.append(parent.getId()).append('\n');
262             for (MCRCategory sibling : parent.getChildren()) {
263                 sb.append(sibling.getId()).append('\n');
264             }
265             LOGGER.debug(sb.toString());
266 
267         }
268         throw new IndexOutOfBoundsException(
269             "Position -1 is not valid: " + getId() + " parent:" + parent.getId() + " children: " + parent
270                 .getChildren()
271                 .stream()
272                 .map(MCRCategory::getId)
273                 .map(MCRCategoryID::getID)
274                 .collect(Collectors.joining(", ")));
275     }
276 
277     /**
278      * @param children
279      *            the children to set
280      */
281     public void setChildren(List<MCRCategory> children) {
282         LOGGER.debug("Set children called for {}list'{}': {}", getId(), children.getClass().getName(), children);
283         childGuard.write(() -> setChildrenUnlocked(children));
284     }
285 
286     @Override
287     protected void setChildrenUnlocked(List<MCRCategory> children) {
288         MCRCategoryChildList newChildren = new MCRCategoryChildList(root, this);
289         newChildren.addAll(children);
290         this.children = newChildren;
291     }
292 
293     /**
294      * @param labels
295      *            the labels to set
296      */
297     public void setLabels(SortedSet<MCRLabel> labels) {
298         this.labels = labels;
299     }
300 
301     /**
302      * @param left
303      *            the left to set
304      */
305     public void setLeft(int left) {
306         this.left = left;
307     }
308 
309     public void setLevel(int level) {
310         this.level = level;
311     }
312 
313     /**
314      * @param right
315      *            the right to set
316      */
317     public void setRight(int right) {
318         this.right = right;
319     }
320 
321     /*
322      * (non-Javadoc)
323      * 
324      * @see org.mycore.datamodel.classifications2.MCRCategory#setRoot(org.mycore.datamodel.classifications2.MCRClassificationObject)
325      */
326     public void setRoot(MCRCategory root) {
327         this.root = root;
328         if (children != null) {
329             setChildren(children);
330         }
331     }
332 
333     static Collection<MCRCategoryImpl> wrapCategories(Collection<? extends MCRCategory> categories, MCRCategory parent,
334         MCRCategory root) {
335         List<MCRCategoryImpl> list = new ArrayList<>(categories.size());
336         for (MCRCategory category : categories) {
337             list.add(wrapCategory(category, parent, root));
338         }
339         return list;
340     }
341 
342     static MCRCategoryImpl wrapCategory(MCRCategory category, MCRCategory parent, MCRCategory root) {
343         MCRCategoryImpl catImpl;
344         if (category.getParent() != null && category.getParent() != parent) {
345             throw new MCRException("MCRCategory is already attached to a different parent.");
346         }
347         if (category instanceof MCRCategoryImpl) {
348             catImpl = (MCRCategoryImpl) category;
349             // don't use setParent() as it call add() from ChildList
350             catImpl.parent = parent;
351             if (root == null) {
352                 root = catImpl;
353             }
354             catImpl.setRoot(root);
355             if (parent != null) {
356                 catImpl.level = parent.getLevel() + 1;
357             } else if (category.isCategory()) {
358                 LOGGER.warn("Something went wrong here, category has no parent and is no root category: {}",
359                     category.getId());
360             }
361             // copy children to temporary list
362             List<MCRCategory> children = new ArrayList<>(catImpl.getChildren().size());
363             children.addAll(catImpl.getChildren());
364             // remove old children
365             catImpl.getChildren().clear();
366             // add new wrapped children
367             catImpl.getChildren().addAll(children);
368             return catImpl;
369         }
370         LOGGER.debug("wrap Category: {}", category.getId());
371         catImpl = new MCRCategoryImpl();
372         catImpl.setId(category.getId());
373         catImpl.labels = category.getLabels();
374         catImpl.parent = parent;
375         if (root == null) {
376             root = catImpl;
377         }
378         catImpl.setRoot(root);
379         catImpl.level = parent.getLevel() + 1;
380         catImpl.children = new ArrayList<>(category.getChildren().size());
381         catImpl.getChildren().addAll(category.getChildren());
382         return catImpl;
383     }
384 
385     @Transient
386     MCRCategoryImpl getLeftSiblingOrOfAncestor() {
387         int index = getPositionInParent();
388         MCRCategoryImpl parent = (MCRCategoryImpl) getParent();
389         if (index > 0) {
390             // has left sibling
391             return (MCRCategoryImpl) parent.getChildren().get(index - 1);
392         }
393         if (parent.getParent() != null) {
394             // recursive call to get left sibling of parent
395             return parent.getLeftSiblingOrOfAncestor();
396         }
397         return parent;// is root
398     }
399 
400     @Transient
401     MCRCategoryImpl getLeftSiblingOrParent() {
402         int index = getPositionInParent();
403         MCRCategoryImpl parent = (MCRCategoryImpl) getParent();
404         if (index == 0) {
405             return parent;
406         }
407         return (MCRCategoryImpl) parent.getChildren().get(index - 1);
408     }
409 
410     @Transient
411     MCRCategoryImpl getRightSiblingOrOfAncestor() {
412         int index = getPositionInParent();
413         MCRCategoryImpl parent = (MCRCategoryImpl) getParent();
414         if (index + 1 < parent.getChildren().size()) {
415             // has right sibling
416             return (MCRCategoryImpl) parent.getChildren().get(index + 1);
417         }
418         if (parent.getParent() != null) {
419             // recursive call to get right sibling of parent
420             return parent.getRightSiblingOrOfAncestor();
421         }
422         return parent;// is root
423     }
424 
425     @Transient
426     MCRCategoryImpl getRightSiblingOrParent() {
427         int index = getPositionInParent();
428         MCRCategoryImpl parent = (MCRCategoryImpl) getParent();
429         if (index + 1 == parent.getChildren().size()) {
430             return parent;
431         }
432         // get Element at index that would be at index+1 after insert
433         return (MCRCategoryImpl) parent.getChildren().get(index + 1);
434     }
435 
436     /**
437      * calculates left and right value throug the subtree rooted at
438      * <code>co</code>.
439      * 
440      * @param leftStart
441      *            this.left will be set to this value
442      * @param levelStart
443      *            this.getLevel() will return this value
444      * @return this.right
445      */
446     public int calculateLeftRightAndLevel(int leftStart, int levelStart) {
447         int curValue = leftStart;
448         final int nextLevel = levelStart + 1;
449         setLeft(leftStart);
450         setLevel(levelStart);
451         for (MCRCategory child : getChildren()) {
452             LOGGER.debug(child.getId());
453             curValue = ((MCRCategoryImpl) child).calculateLeftRightAndLevel(++curValue, nextLevel);
454         }
455         setRight(++curValue);
456         return curValue;
457     }
458 
459     /**
460      * @param internalID
461      *            the internalID to set
462      */
463     public void setInternalID(int internalID) {
464         this.internalID = internalID;
465     }
466 
467     public void setRootID(String rootID) {
468         if (getId() == null) {
469             setId(MCRCategoryID.rootID(rootID));
470         } else if (!getId().getRootID().equals(rootID)) {
471             setId(new MCRCategoryID(rootID, getId().getID()));
472         }
473     }
474 
475     public void setCategID(String categID) {
476         if (getId() == null) {
477             setId(new MCRCategoryID(null, categID));
478         } else if (!getId().getID().equals(categID)) {
479             setId(new MCRCategoryID(getId().getRootID(), categID));
480         }
481     }
482 
483     @Override
484     public int hashCode() {
485         final int prime = 31;
486         int result = 1;
487         result = prime * result + internalID;
488         result = prime * result + left;
489         result = prime * result + level;
490         result = prime * result + right;
491         result = prime * result + getId().hashCode();
492         return result;
493     }
494 
495     @Override
496     public boolean equals(Object obj) {
497         if (this == obj) {
498             return true;
499         }
500         if (obj == null) {
501             return false;
502         }
503         if (getClass() != obj.getClass()) {
504             return false;
505         }
506         MCRCategoryImpl other = (MCRCategoryImpl) obj;
507         if (internalID != other.internalID) {
508             return false;
509         }
510         if (left != other.left) {
511             return false;
512         }
513         if (level != other.level) {
514             return false;
515         }
516         return right == other.right;
517     }
518 
519     @Transient
520     public String getRootID() {
521         return getId().getRootID();
522     }
523 
524 }