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.frontend.cli;
20  
21  import java.io.File;
22  import java.io.FileOutputStream;
23  import java.io.IOException;
24  import java.net.MalformedURLException;
25  import java.net.URISyntaxException;
26  import java.net.URL;
27  import java.nio.file.Paths;
28  import java.util.ArrayList;
29  import java.util.Arrays;
30  import java.util.Collections;
31  import java.util.List;
32  import java.util.Locale;
33  import java.util.stream.Collectors;
34  
35  import javax.xml.transform.Transformer;
36  import javax.xml.transform.TransformerConfigurationException;
37  import javax.xml.transform.TransformerException;
38  import javax.xml.transform.TransformerFactory;
39  import javax.xml.transform.TransformerFactoryConfigurationError;
40  import javax.xml.transform.stream.StreamResult;
41  import javax.xml.transform.stream.StreamSource;
42  
43  import org.apache.logging.log4j.LogManager;
44  import org.apache.logging.log4j.Logger;
45  import org.jdom2.Document;
46  import org.jdom2.output.Format;
47  import org.jdom2.output.XMLOutputter;
48  import org.jdom2.transform.JDOMSource;
49  import org.mycore.backend.jpa.MCREntityManagerProvider;
50  import org.mycore.common.MCRConstants;
51  import org.mycore.common.MCRException;
52  import org.mycore.common.content.MCRSourceContent;
53  import org.mycore.common.content.MCRURLContent;
54  import org.mycore.common.xml.MCRURIResolver;
55  import org.mycore.common.xml.MCRXMLParserFactory;
56  import org.mycore.datamodel.classifications2.MCRCategory;
57  import org.mycore.datamodel.classifications2.MCRCategoryDAO;
58  import org.mycore.datamodel.classifications2.MCRCategoryDAOFactory;
59  import org.mycore.datamodel.classifications2.MCRCategoryID;
60  import org.mycore.datamodel.classifications2.MCRLabel;
61  import org.mycore.datamodel.classifications2.MCRUnmappedCategoryRemover;
62  import org.mycore.datamodel.classifications2.impl.MCRCategoryDAOImpl;
63  import org.mycore.datamodel.classifications2.impl.MCRCategoryImpl;
64  import org.mycore.datamodel.classifications2.utils.MCRCategoryTransformer;
65  import org.mycore.datamodel.classifications2.utils.MCRXMLTransformer;
66  import org.mycore.datamodel.common.MCRActiveLinkException;
67  import org.mycore.frontend.cli.annotation.MCRCommand;
68  import org.mycore.frontend.cli.annotation.MCRCommandGroup;
69  import org.xml.sax.SAXParseException;
70  
71  import jakarta.persistence.EntityManager;
72  
73  /**
74   * Commands for the classification system.
75   *
76   * @author Thomas Scheffler (yagee)
77   */
78  @MCRCommandGroup(name = "Classification Commands")
79  public class MCRClassification2Commands extends MCRAbstractCommands {
80      private static Logger LOGGER = LogManager.getLogger();
81  
82      private static final MCRCategoryDAO DAO = MCRCategoryDAOFactory.getInstance();
83  
84      /** Default transformer script */
85      public static final String DEFAULT_TRANSFORMER = "save-classification.xsl";
86  
87      /**
88       * Deletes a classification
89       *
90       * @param classID classification ID
91       * @see MCRCategoryDAO#deleteCategory(MCRCategoryID)
92       */
93      @MCRCommand(syntax = "delete classification {0}",
94          help = "The command remove the classification with MCRObjectID {0} from the system.",
95          order = 30)
96      public static void delete(String classID) {
97          DAO.deleteCategory(MCRCategoryID.rootID(classID));
98      }
99  
100     /**
101      * Counts the classification categories on top level
102      *
103      * @param classID classification ID
104      */
105     @MCRCommand(syntax = "count classification children of {0}",
106         help = "The command count the categoies of the classification with MCRObjectID {0} in the system.",
107         order = 80)
108     public static void countChildren(String classID) {
109         MCRCategory category = DAO.getCategory(MCRCategoryID.rootID(classID), 1);
110         System.out.printf(Locale.ROOT, "%s has %d children", category.getId(), category.getChildren().size());
111     }
112 
113     /**
114      * Adds a classification.
115      *
116      * Classification is built from a file.
117      *
118      * @param filename
119      *            file in mcrclass xml format
120      * @see MCRCategoryDAO#addCategory(MCRCategoryID, MCRCategory)
121      */
122     @MCRCommand(syntax = "load classification from file {0}",
123         help = "The command adds a new classification from file {0} to the system.",
124         order = 10)
125     public static List<String> loadFromFile(String filename) throws URISyntaxException, MCRException, SAXParseException,
126         IOException {
127         String fileURL = Paths.get(filename).toAbsolutePath().normalize().toUri().toURL().toString();
128         return Collections.singletonList("load classification from url " + fileURL);
129     }
130 
131     @MCRCommand(syntax = "load classification from url {0}",
132         help = "The command adds a new classification from URL {0} to the system.",
133         order = 15)
134     public static void loadFromURL(String fileURL) throws SAXParseException, MalformedURLException, URISyntaxException {
135         Document xml = MCRXMLParserFactory.getParser().parseXML(new MCRURLContent(new URL(fileURL)));
136         MCRCategory category = MCRXMLTransformer.getCategory(xml);
137         DAO.addCategory(null, category);
138     }
139 
140     @MCRCommand(syntax = "load classification from uri {0}",
141         help = "The command adds a new classification from URI {0} to the system.",
142         order = 17)
143     public static void loadFromURI(String fileURI) throws SAXParseException, URISyntaxException, TransformerException {
144         Document xml = MCRXMLParserFactory.getParser().parseXML(MCRSourceContent.getInstance(fileURI));
145         MCRCategory category = MCRXMLTransformer.getCategory(xml);
146         DAO.addCategory(null, category);
147     }
148 
149     /**
150      * Replaces a classification with a new version
151      *
152      * @param filename
153      *            file in mcrclass xml format
154      * @see MCRCategoryDAO#replaceCategory(MCRCategory)
155      */
156     @MCRCommand(syntax = "update classification from file {0}",
157         help = "The command updates a classification from file {0} to the system.",
158         order = 20)
159     public static List<String> updateFromFile(String filename)
160         throws URISyntaxException, MCRException, SAXParseException,
161         IOException {
162         String fileURL = Paths.get(filename).toAbsolutePath().normalize().toUri().toURL().toString();
163         return Collections.singletonList("update classification from url " + fileURL);
164     }
165 
166     @MCRCommand(syntax = "update classification from url {0}",
167         help = "The command updates a classification from URL {0} to the system.",
168         order = 25)
169     public static void updateFromURL(String fileURL)
170         throws SAXParseException, MalformedURLException, URISyntaxException {
171         Document xml = MCRXMLParserFactory.getParser().parseXML(new MCRURLContent(new URL(fileURL)));
172         MCRCategory category = MCRXMLTransformer.getCategory(xml);
173         if (DAO.exist(category.getId())) {
174             DAO.replaceCategory(category);
175         } else {
176             // add if classification does not exist
177             DAO.addCategory(null, category);
178         }
179     }
180 
181     @MCRCommand(syntax = "update classification from uri {0}",
182         help = "The command updates a classification from URI {0} to the system.",
183         order = 27)
184     public static void updateFromURI(String fileURI) throws SAXParseException, URISyntaxException,
185         TransformerException {
186         Document xml = MCRXMLParserFactory.getParser().parseXML(MCRSourceContent.getInstance(fileURI));
187         MCRCategory category = MCRXMLTransformer.getCategory(xml);
188         if (DAO.exist(category.getId())) {
189             DAO.replaceCategory(category);
190         } else {
191             // add if classification does not exist
192             DAO.addCategory(null, category);
193         }
194     }
195 
196     /**
197      * Loads MCRClassification from all XML files in a directory.
198      *
199      * @param directory
200      *            the directory containing the XML files
201      */
202     @MCRCommand(syntax = "load all classifications from directory {0}",
203         help = "The command add all classifications in the directory {0} to the system.",
204         order = 40)
205     public static List<String> loadFromDirectory(String directory) throws MCRActiveLinkException {
206         return processFromDirectory(directory, false);
207     }
208 
209     /**
210      * Updates MCRClassification from all XML files in a directory.
211      *
212      * @param directory
213      *            the directory containing the XML files
214      */
215     @MCRCommand(syntax = "update all classifications from directory {0}",
216         help = "The command update all classifications in the directory {0} to the system.",
217         order = 50)
218     public static List<String> updateFromDirectory(String directory) throws MCRActiveLinkException {
219         return processFromDirectory(directory, true);
220     }
221 
222     /**
223      * Loads or updates MCRClassification from all XML files in a directory.
224      *
225      * @param directory
226      *            the directory containing the XML files
227      * @param update
228      *            if true, classification will be updated, else Classification
229      *            is created
230      * @throws MCRActiveLinkException
231      */
232     private static List<String> processFromDirectory(String directory, boolean update) throws MCRActiveLinkException {
233         File dir = new File(directory);
234 
235         if (!dir.isDirectory()) {
236             LOGGER.warn("{} ignored, is not a directory.", directory);
237             return null;
238         }
239 
240         String[] list = dir.list();
241 
242         if (list.length == 0) {
243             LOGGER.warn("No files found in directory {}", directory);
244             return null;
245         }
246 
247         return Arrays.stream(list)
248             .filter(file -> file.endsWith(".xml"))
249             .map(file -> (update ? "update" : "load") + " classification from file " + new File(
250                 dir, file).getAbsolutePath())
251             .collect(Collectors.toList());
252     }
253 
254     /**
255      * Save a MCRClassification.
256      *
257      * @param id
258      *            the ID of the MCRClassification to be save.
259      * @param dirname
260      *            the directory to export the classification to
261      * @param style
262      *            the name part of the stylesheet like <em>style</em>
263      *            -classification.xsl
264      * @return false if an error was occured, else true
265      */
266     @MCRCommand(syntax = "export classification {0} to directory {1} with {2}",
267         help = "The command exports the classification with MCRObjectID {0} as xml file to directory named {1} "
268             + "using the stylesheet {2}-object.xsl. For {2} save is the default.",
269         order = 60)
270     public static boolean export(String id, String dirname, String style) throws Exception {
271         String dname = "";
272         if (dirname.length() != 0) {
273             try {
274                 File dir = new File(dirname);
275                 if (!dir.isDirectory()) {
276                     dir.mkdir();
277                 }
278                 if (!dir.isDirectory()) {
279                     LOGGER.error("Can't find or create directory {}", dir.getAbsolutePath());
280                     return false;
281                 } else {
282                     dname = dirname;
283                 }
284             } catch (Exception e) {
285                 LOGGER.error("Can't find or create directory {}", dirname, e);
286                 return false;
287             }
288         }
289         MCRCategory cl = DAO.getCategory(MCRCategoryID.rootID(id), -1);
290         Document classDoc = MCRCategoryTransformer.getMetaDataDocument(cl, false);
291 
292         Transformer trans = getTransformer(style);
293         File xmlOutput = new File(dname, id + ".xml");
294         FileOutputStream out = new FileOutputStream(xmlOutput);
295         if (trans != null) {
296             StreamResult sr = new StreamResult(out);
297             trans.transform(new JDOMSource(classDoc), sr);
298         } else {
299             XMLOutputter xout = new XMLOutputter(Format.getPrettyFormat());
300             xout.output(classDoc, out);
301             out.flush();
302         }
303         LOGGER.info("Classifcation {} saved to {}.", id, xmlOutput.getCanonicalPath());
304         return true;
305     }
306 
307     /**
308      * The method search for a stylesheet mcr_<em>style</em>_object.xsl and
309      * build the transformer. Default is <em>mcr_save-object.xsl</em>.
310      *
311      * @param style
312      *            the style attribute for the transformer stylesheet
313      * @return the transformer
314      * @throws TransformerFactoryConfigurationError
315      * @throws TransformerConfigurationException
316      */
317     private static Transformer getTransformer(String style) throws TransformerFactoryConfigurationError,
318         TransformerConfigurationException {
319         String xslfile = DEFAULT_TRANSFORMER;
320         if (style != null && style.trim().length() != 0) {
321             xslfile = style + "-classification.xsl";
322         }
323         Transformer trans = null;
324 
325         URL styleURL = MCRClassification2Commands.class.getResource("/" + xslfile);
326         if (styleURL == null) {
327             styleURL = MCRClassification2Commands.class.getResource(DEFAULT_TRANSFORMER);
328         }
329         if (styleURL != null) {
330             StreamSource source;
331             try {
332                 source = new StreamSource(styleURL.toURI().toASCIIString());
333             } catch (URISyntaxException e) {
334                 throw new TransformerConfigurationException(e);
335             }
336             TransformerFactory transfakt = TransformerFactory.newInstance();
337             transfakt.setURIResolver(MCRURIResolver.instance());
338             trans = transfakt.newTransformer(source);
339         }
340         return trans;
341     }
342 
343     /**
344      * Save all MCRClassifications.
345      *
346      * @param dirname
347      *            the directory name to store all classifications
348      * @param style
349      *            the name part of the stylesheet like <em>style</em>
350      *            -classification.xsl
351      * @return false if an error was occured, else true
352      */
353     @MCRCommand(syntax = "export all classifications to directory {0} with {1}",
354         help = "The command store all classifications to the directory with name {0} with the stylesheet "
355             + "{1}-object.xsl. For {1} save is the default.",
356         order = 70)
357     public static boolean exportAll(String dirname, String style) throws Exception {
358         List<MCRCategoryID> allClassIds = DAO.getRootCategoryIDs();
359         boolean ret = false;
360         for (MCRCategoryID id : allClassIds) {
361             ret = ret & export(id.getRootID(), dirname, style);
362         }
363         return ret;
364     }
365 
366     /**
367      * List all IDs of all classifications stored in the database
368      */
369     @MCRCommand(syntax = "list all classifications",
370         help = "The command list all classification stored in the database.",
371         order = 100)
372     public static void listAllClassifications() {
373         List<MCRCategoryID> allClassIds = DAO.getRootCategoryIDs();
374         for (MCRCategoryID id : allClassIds) {
375             LOGGER.info(id.getRootID());
376         }
377         LOGGER.info("");
378     }
379 
380     /**
381      * List a MCRClassification.
382      *
383      * @param classid
384      *            the MCRObjectID of the classification
385      */
386     @MCRCommand(syntax = "list classification {0}",
387         help = "The command list the classification with MCRObjectID {0}.",
388         order = 90)
389     public static void listClassification(String classid) {
390         MCRCategoryID clid = MCRCategoryID.rootID(classid);
391         MCRCategory cl = DAO.getCategory(clid, -1);
392         LOGGER.info(classid);
393         if (cl != null) {
394             listCategory(cl);
395         } else {
396             LOGGER.error("Can't find classification {}", classid);
397         }
398     }
399 
400     private static void listCategory(MCRCategory categ) {
401         int level = categ.getLevel();
402         StringBuilder sb = new StringBuilder(128);
403         for (int i = 0; i < level * 2; i++) {
404             sb.append(' ');
405         }
406         String space = sb.toString();
407         if (categ.isCategory()) {
408             LOGGER.info("{}  ID    : {}", space, categ.getId().getID());
409         }
410         for (MCRLabel label : categ.getLabels()) {
411             LOGGER.info("{}  Label : ({}) {}", space, label.getLang(), label.getText());
412         }
413         List<MCRCategory> children = categ.getChildren();
414         for (MCRCategory child : children) {
415             listCategory(child);
416         }
417     }
418 
419     @MCRCommand(syntax = "repair category with empty labels",
420         help = "fixes all categories with no labels (adds a label with categid as @text for default lang)",
421         order = 110)
422     public static void repairEmptyLabels() {
423         EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
424         String deleteEmptyLabels = "delete from {h-schema}MCRCategoryLabels where text is null or trim(text) = ''";
425         int affected = em.createNativeQuery(deleteEmptyLabels).executeUpdate();
426         LOGGER.info("Deleted {} labels.", affected);
427         String sqlQuery = "select cat.classid,cat.categid from {h-schema}MCRCategory cat "
428             + "left outer join {h-schema}MCRCategoryLabels label on cat.internalid = label.category where "
429             + "label.text is null";
430         @SuppressWarnings("unchecked")
431         List<Object[]> list = em.createNativeQuery(sqlQuery).getResultList();
432 
433         for (Object resultList : list) {
434             Object[] arrayOfResults = (Object[]) resultList;
435             String classIDString = (String) arrayOfResults[0];
436             String categIDString = (String) arrayOfResults[1];
437 
438             MCRCategoryID mcrCategID = new MCRCategoryID(classIDString, categIDString);
439             MCRLabel mcrCategLabel = new MCRLabel(MCRConstants.DEFAULT_LANG, categIDString, null);
440             MCRCategoryDAOFactory.getInstance().setLabel(mcrCategID, mcrCategLabel);
441             LOGGER.info("fixing category with class ID \"{}\" and category ID \"{}\"", classIDString, categIDString);
442         }
443         LOGGER.info("Fixing category labels completed!");
444     }
445 
446     @MCRCommand(syntax = "repair position in parent",
447         help = "fixes all categories gaps in position in parent",
448         order = 120)
449     @SuppressWarnings("unchecked")
450     public static void repairPositionInParent() {
451         EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
452         // this SQL-query find missing numbers in positioninparent
453         String sqlQuery = "select parentid, min(cat1.positioninparent+1) from {h-schema}MCRCategory cat1 "
454             + "where cat1.parentid is not null and not exists" + "(select 1 from {h-schema}MCRCategory cat2 "
455             + "where cat2.parentid=cat1.parentid and cat2.positioninparent=(cat1.positioninparent+1))"
456             + "and cat1.positioninparent not in "
457             + "(select max(cat3.positioninparent) from {h-schema}MCRCategory cat3 "
458             + "where cat3.parentid=cat1.parentid) group by cat1.parentid";
459 
460         for (List<Object[]> parentWithErrorsList = em.createNativeQuery(sqlQuery)
461             .getResultList(); !parentWithErrorsList
462                 .isEmpty();
463             parentWithErrorsList = em.createNativeQuery(sqlQuery).getResultList()) {
464             for (Object[] parentWithErrors : parentWithErrorsList) {
465                 Number parentID = (Number) parentWithErrors[0];
466                 Number firstErrorPositionInParent = (Number) parentWithErrors[1];
467                 LOGGER.info("Category {} has the missing position {} ...", parentID, firstErrorPositionInParent);
468                 repairCategoryWithGapInPos(parentID, firstErrorPositionInParent);
469                 LOGGER.info("Fixed position {} for category {}.", firstErrorPositionInParent, parentID);
470             }
471         }
472 
473         sqlQuery = "select parentid, min(cat1.positioninparent-1) from {h-schema}MCRCategory cat1 "
474             + "where cat1.parentid is not null and not exists" + "(select 1 from {h-schema}MCRCategory cat2 "
475             + "where cat2.parentid=cat1.parentid and cat2.positioninparent=(cat1.positioninparent-1))"
476             + "and cat1.positioninparent not in "
477             + "(select max(cat3.positioninparent) from {h-schema}MCRCategory cat3 "
478             + "where cat3.parentid=cat1.parentid) and cat1.positioninparent > 0 group by cat1.parentid";
479 
480         while (true) {
481             List<Object[]> parentWithErrorsList = em.createNativeQuery(sqlQuery).getResultList();
482 
483             if (parentWithErrorsList.isEmpty()) {
484                 break;
485             }
486 
487             for (Object[] parentWithErrors : parentWithErrorsList) {
488                 Number parentID = (Number) parentWithErrors[0];
489                 Number wrongStartPositionInParent = (Number) parentWithErrors[1];
490                 LOGGER.info("Category {} has the the starting position {} ...", parentID, wrongStartPositionInParent);
491                 repairCategoryWithWrongStartPos(parentID, wrongStartPositionInParent);
492                 LOGGER.info("Fixed position {} for category {}.", wrongStartPositionInParent, parentID);
493             }
494         }
495         LOGGER.info("Repair position in parent finished!");
496     }
497 
498     public static void repairCategoryWithWrongStartPos(Number parentID, Number wrongStartPositionInParent) {
499         EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
500         String sqlQuery = "update {h-schema}MCRCategory set positioninparent= positioninparent -"
501             + wrongStartPositionInParent
502             + "-1 where parentid=" + parentID + " and positioninparent > " + wrongStartPositionInParent;
503 
504         em.createNativeQuery(sqlQuery).executeUpdate();
505     }
506 
507     private static void repairCategoryWithGapInPos(Number parentID, Number firstErrorPositionInParent) {
508         EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
509         // the query decrease the position in parent with a rate.
510         // eg. posInParent: 0 1 2 5 6 7
511         // at 3 the position get faulty, 5 is the min. of the position greather
512         // 3
513         // so the reate is 5-3 = 2
514         String sqlQuery = "update {h-schema}MCRCategory "
515             + "set positioninparent=(positioninparent - (select min(positioninparent) from "
516             + "{h-schema}MCRCategory where parentid="
517             + parentID
518             + " and positioninparent > "
519             + firstErrorPositionInParent
520             + ")+"
521             + firstErrorPositionInParent
522             + ") where parentid=" + parentID + " and positioninparent > " + firstErrorPositionInParent;
523 
524         em.createNativeQuery(sqlQuery).executeUpdate();
525     }
526 
527     @MCRCommand(syntax = "repair left right values for classification {0}",
528         help = "fixes all left and right values in the given classification",
529         order = 130)
530     public static void repairLeftRightValue(String classID) {
531         if (!(DAO instanceof MCRCategoryDAOImpl)) {
532             LOGGER.error("Command not compatible with {}", DAO.getClass().getName());
533             return;
534         }
535         ((MCRCategoryDAOImpl) DAO).repairLeftRightValue(classID);
536     }
537 
538     @MCRCommand(syntax = "check all classifications",
539         help = "checks if all redundant information are stored without conflicts",
540         order = 140)
541     public static List<String> checkAllClassifications() {
542         List<MCRCategoryID> classifications = MCRCategoryDAOFactory.getInstance().getRootCategoryIDs();
543         List<String> commands = new ArrayList<>(classifications.size());
544         for (MCRCategoryID id : classifications) {
545             commands.add("check classification " + id.getRootID());
546         }
547         return commands;
548     }
549 
550     @MCRCommand(syntax = "check classification {0}",
551         help = "checks if all redundant information are stored without conflicts",
552         order = 150)
553     public static void checkClassification(String id) {
554         LOGGER.info("Checking classifcation {}", id);
555         ArrayList<String> log = new ArrayList<>();
556         LOGGER.info("{}: checking for missing parentID", id);
557         checkMissingParent(id, log);
558         LOGGER.info("{}: checking for empty labels", id);
559         checkEmptyLabels(id, log);
560         if (log.isEmpty()) {
561             MCRCategoryImpl category = (MCRCategoryImpl) MCRCategoryDAOFactory.getInstance().getCategory(
562                 MCRCategoryID.rootID(id), -1);
563             LOGGER.info("{}: checking left, right and level values and for non-null children", id);
564             checkLeftRightAndLevel(category, 0, 0, log);
565         }
566         if (log.size() > 0) {
567             LOGGER.error("Some errors occured on last test, report will follow");
568             StringBuilder sb = new StringBuilder();
569             for (String msg : log) {
570                 sb.append(msg).append('\n');
571             }
572             LOGGER.error("Error report for classification {}\n{}", id, sb);
573         } else {
574             LOGGER.info("Classifcation {} has no errors.", id);
575         }
576     }
577 
578     @MCRCommand(syntax = "remove unmapped categories from classification {0}",
579         help = "Deletes all Categories of classification {0} which can not be mapped from all other classifications!",
580         order = 160)
581     public static void filterClassificationWithMapping(String id) {
582         new MCRUnmappedCategoryRemover(id).filter();
583     }
584 
585     private static int checkLeftRightAndLevel(MCRCategoryImpl category, int leftStart, int levelStart,
586         List<String> log) {
587         int curValue = leftStart;
588         final int nextLevel = levelStart + 1;
589         if (leftStart != category.getLeft()) {
590             log.add("LEFT of " + category.getId() + " is " + category.getLeft() + " should be " + leftStart);
591         }
592         if (levelStart != category.getLevel()) {
593             log.add("LEVEL of " + category.getId() + " is " + category.getLevel() + " should be " + levelStart);
594         }
595         int position = 0;
596         for (MCRCategory child : category.getChildren()) {
597             if (child == null) {
598                 log.add("NULL child of parent " + category.getId() + " on position " + position);
599                 continue;
600             }
601             LOGGER.debug(child.getId());
602             curValue = checkLeftRightAndLevel((MCRCategoryImpl) child, ++curValue, nextLevel, log);
603             position++;
604         }
605         ++curValue;
606         if (curValue != category.getRight()) {
607             log.add("RIGHT of " + category.getId() + " is " + category.getRight() + " should be " + curValue);
608         }
609         return curValue;
610     }
611 
612     private static void checkEmptyLabels(String classID, List<String> log) {
613         EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
614         String sqlQuery = "select cat.categid from {h-schema}MCRCategory cat "
615             + "left outer join {h-schema}MCRCategoryLabels label on cat.internalid = label.category where "
616             + "cat.classid='"
617             + classID + "' and (label.text is null or trim(label.text) = '')";
618         @SuppressWarnings("unchecked")
619         List<String> list = em.createNativeQuery(sqlQuery).getResultList();
620 
621         for (String categIDString : list) {
622             log.add("EMPTY lables for category " + new MCRCategoryID(classID, categIDString));
623         }
624     }
625 
626     private static void checkMissingParent(String classID, List<String> log) {
627         EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
628         String sqlQuery = "select cat.categid from {h-schema}MCRCategory cat WHERE cat.classid='"
629             + classID + "' and cat.level > 0 and cat.parentID is NULL";
630         @SuppressWarnings("unchecked")
631         List<String> list = em.createNativeQuery(sqlQuery).getResultList();
632 
633         for (String categIDString : list) {
634             log.add("parentID is null for category " + new MCRCategoryID(classID, categIDString));
635         }
636     }
637 
638     @MCRCommand(syntax = "repair missing parent for classification {0}",
639         help = "restores parentID from information in the given classification, if left right values are correct",
640         order = 130)
641     public static void repairMissingParent(String classID) {
642         EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
643         String sqlQuery = "update {h-schema}MCRCategory cat set cat.parentID=(select parent.internalID from "
644             + "{h-schema}MCRCategory parent WHERE parent.classid='"
645             + classID
646             + "' and parent.leftValue<cat.leftValue and parent.rightValue>cat.rightValue and "
647             + "parent.level=(cat.level-1)) WHERE cat.classid='"
648             + classID + "' and cat.level > 0 and cat.parentID is NULL";
649         int updates = em.createNativeQuery(sqlQuery).executeUpdate();
650         LOGGER.info(() -> "Repaired " + updates + " parentID columns for classification " + classID);
651     }
652 }