org.mycore.frontend.cli.MCRClassification2Commands.java Source code

Java tutorial

Introduction

Here is the source code for org.mycore.frontend.cli.MCRClassification2Commands.java

Source

/**
 *
 * $Revision$ $Date$
 *
 * This file is part of ** M y C o R e **
 * Visit our homepage at http://www.mycore.de/ for details.
 *
 * This program is free software; you can use it, redistribute it
 * and / or modify it under the terms of the GNU General Public License
 * (GPL) as published by the Free Software Foundation; either version 2
 * of the License or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program, normally in the file license.txt.
 * If not, write to the Free Software Foundation Inc.,
 * 59 Temple Place - Suite 330, Boston, MA  02111-1307 USA
 *
 **/
package org.mycore.frontend.cli;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;

import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.hibernate.Session;
import org.jdom2.Document;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;
import org.mycore.backend.hibernate.MCRHIBConnection;
import org.mycore.common.MCRConstants;
import org.mycore.common.MCRException;
import org.mycore.common.content.MCRURLContent;
import org.mycore.common.xml.MCRURIResolver;
import org.mycore.common.xml.MCRXMLParserFactory;
import org.mycore.datamodel.classifications2.MCRCategory;
import org.mycore.datamodel.classifications2.MCRCategoryDAO;
import org.mycore.datamodel.classifications2.MCRCategoryDAOFactory;
import org.mycore.datamodel.classifications2.MCRCategoryID;
import org.mycore.datamodel.classifications2.MCRLabel;
import org.mycore.datamodel.classifications2.impl.MCRCategoryDAOImpl;
import org.mycore.datamodel.classifications2.impl.MCRCategoryImpl;
import org.mycore.datamodel.classifications2.utils.MCRCategoryTransformer;
import org.mycore.datamodel.classifications2.utils.MCRXMLTransformer;
import org.mycore.datamodel.common.MCRActiveLinkException;
import org.mycore.frontend.cli.annotation.MCRCommand;
import org.mycore.frontend.cli.annotation.MCRCommandGroup;
import org.xml.sax.SAXParseException;

/**
 * Commands for the classification system.
 *
 * @author Thomas Scheffler (yagee)
 */
@MCRCommandGroup(name = "Classification Commands")
public class MCRClassification2Commands extends MCRAbstractCommands {
    private static Logger LOGGER = LogManager.getLogger();

    private static final MCRCategoryDAO DAO = MCRCategoryDAOFactory.getInstance();

    /** Default transformer script */
    public static final String DEFAULT_TRANSFORMER = "save-classification.xsl";

    /**
     * Deletes a classification
     *
     * @param classID classification ID
     * @see MCRCategoryDAO#deleteCategory(MCRCategoryID)
     */
    @MCRCommand(syntax = "delete classification {0}", help = "The command remove the classification with MCRObjectID {0} from the system.", order = 30)
    public static void delete(String classID) {
        DAO.deleteCategory(MCRCategoryID.rootID(classID));
    }

    /**
     * Counts the classification categories on top level
     *
     * @param classID classification ID
     */
    @MCRCommand(syntax = "count classification children of {0}", help = "The command count the categoies of the classification with MCRObjectID {0} in the system.", order = 80)
    public static void countChildren(String classID) {
        MCRCategory category = DAO.getCategory(MCRCategoryID.rootID(classID), 1);
        System.out.printf(Locale.ROOT, "%s has %d children", category.getId(), category.getChildren().size());
    }

    /**
     * Adds a classification.
     *
     * Classification is built from a file.
     *
     * @param filename
     *            file in mcrclass xml format
     * @see MCRCategoryDAO#addCategory(MCRCategoryID, MCRCategory)
     */
    @MCRCommand(syntax = "load classification from file {0}", help = "The command adds a new classification from file {0} to the system.", order = 10)
    public static List<String> loadFromFile(String filename)
            throws URISyntaxException, MCRException, SAXParseException, IOException {
        String fileURL = Paths.get(filename).toAbsolutePath().normalize().toUri().toURL().toString();
        return Collections.singletonList("load classification from url " + fileURL);
    }

    @MCRCommand(syntax = "load classification from url {0}", help = "The command adds a new classification from URL {0} to the system.", order = 15)
    public static void loadFromURL(String fileURL)
            throws SAXParseException, MalformedURLException, URISyntaxException {
        Document xml = MCRXMLParserFactory.getParser().parseXML(new MCRURLContent(new URL(fileURL)));
        MCRCategory category = MCRXMLTransformer.getCategory(xml);
        DAO.addCategory(null, category);
    }

    /**
     * Replaces a classification with a new version
     *
     * @param filename
     *            file in mcrclass xml format
     * @see MCRCategoryDAO#replaceCategory(MCRCategory)
     */
    @MCRCommand(syntax = "update classification from file {0}", help = "The command updates a classification from file {0} to the system.", order = 20)
    public static List<String> updateFromFile(String filename)
            throws URISyntaxException, MCRException, SAXParseException, IOException {
        String fileURL = Paths.get(filename).toAbsolutePath().normalize().toUri().toURL().toString();
        return Collections.singletonList("update classification from url " + fileURL);
    }

    @MCRCommand(syntax = "update classification from url {0}", help = "The command updates a classification from URL {0} to the system.", order = 25)
    public static void updateFromURL(String fileURL)
            throws SAXParseException, MalformedURLException, URISyntaxException {
        Document xml = MCRXMLParserFactory.getParser().parseXML(new MCRURLContent(new URL(fileURL)));
        MCRCategory category = MCRXMLTransformer.getCategory(xml);
        if (DAO.exist(category.getId())) {
            DAO.replaceCategory(category);
        } else {
            // add if classification does not exist
            DAO.addCategory(null, category);
        }
    }

    /**
     * Loads MCRClassification from all XML files in a directory.
     *
     * @param directory
     *            the directory containing the XML files
     */
    @MCRCommand(syntax = "load all classifications from directory {0}", help = "The command add all classifications in the directory {0} to the system.", order = 40)
    public static List<String> loadFromDirectory(String directory) throws MCRActiveLinkException {
        return processFromDirectory(directory, false);
    }

    /**
     * Updates MCRClassification from all XML files in a directory.
     *
     * @param directory
     *            the directory containing the XML files
     */
    @MCRCommand(syntax = "update all classifications from directory {0}", help = "The command update all classifications in the directory {0} to the system.", order = 50)
    public static List<String> updateFromDirectory(String directory) throws MCRActiveLinkException {
        return processFromDirectory(directory, true);
    }

    /**
     * Loads or updates MCRClassification from all XML files in a directory.
     *
     * @param directory
     *            the directory containing the XML files
     * @param update
     *            if true, classification will be updated, else Classification
     *            is created
     * @throws MCRActiveLinkException
     */
    private static List<String> processFromDirectory(String directory, boolean update)
            throws MCRActiveLinkException {
        File dir = new File(directory);

        if (!dir.isDirectory()) {
            LOGGER.warn(directory + " ignored, is not a directory.");
            return null;
        }

        String[] list = dir.list();

        if (list.length == 0) {
            LOGGER.warn("No files found in directory " + directory);
            return null;
        }

        return Arrays
                .stream(list).filter(file -> file.endsWith(".xml")).map(file -> (update ? "update" : "load")
                        + " classification from file " + new File(dir, file).getAbsolutePath())
                .collect(Collectors.toList());
    }

    /**
     * Save a MCRClassification.
     *
     * @param ID
     *            the ID of the MCRClassification to be save.
     * @param dirname
     *            the directory to export the classification to
     * @param style
     *            the name part of the stylesheet like <em>style</em>
     *            -classification.xsl
     * @return false if an error was occured, else true
     */
    @MCRCommand(syntax = "export classification {0} to directory {1} with {2}", help = "The command exports the classification with MCRObjectID {0} as xml file to directory named {1} using the stylesheet {2}-object.xsl. For {2} save is the default.", order = 60)
    public static boolean export(String ID, String dirname, String style) throws Exception {
        String dname = "";
        if (dirname.length() != 0) {
            try {
                File dir = new File(dirname);
                if (!dir.isDirectory()) {
                    dir.mkdir();
                }
                if (!dir.isDirectory()) {
                    LOGGER.error("Can't find or create directory " + dir.getAbsolutePath());
                    return false;
                } else {
                    dname = dirname;
                }
            } catch (Exception e) {
                LOGGER.error("Can't find or create directory " + dirname, e);
                return false;
            }
        }
        MCRCategory cl = DAO.getCategory(MCRCategoryID.rootID(ID), -1);
        Document classDoc = MCRCategoryTransformer.getMetaDataDocument(cl, false);

        Transformer trans = getTransformer(style);
        File xmlOutput = new File(dname, ID + ".xml");
        FileOutputStream out = new FileOutputStream(xmlOutput);
        if (trans != null) {
            StreamResult sr = new StreamResult(out);
            trans.transform(new org.jdom2.transform.JDOMSource(classDoc), sr);
        } else {
            XMLOutputter xout = new XMLOutputter(Format.getPrettyFormat());
            xout.output(classDoc, out);
            out.flush();
        }
        LOGGER.info("Classifcation " + ID + " saved to " + xmlOutput.getCanonicalPath() + ".");
        return true;
    }

    /**
     * The method search for a stylesheet mcr_<em>style</em>_object.xsl and
     * build the transformer. Default is <em>mcr_save-object.xsl</em>.
     *
     * @param style
     *            the style attribute for the transformer stylesheet
     * @return the transformer
     * @throws TransformerFactoryConfigurationError
     * @throws TransformerConfigurationException
     */
    private static Transformer getTransformer(String style)
            throws TransformerFactoryConfigurationError, TransformerConfigurationException {
        String xslfile = DEFAULT_TRANSFORMER;
        if (style != null && style.trim().length() != 0) {
            xslfile = style + "-classification.xsl";
        }
        Transformer trans = null;

        URL styleURL = MCRClassification2Commands.class.getResource("/" + xslfile);
        if (styleURL == null) {
            styleURL = MCRClassification2Commands.class.getResource(DEFAULT_TRANSFORMER);
        }
        if (styleURL != null) {
            StreamSource source;
            try {
                source = new StreamSource(styleURL.toURI().toASCIIString());
            } catch (URISyntaxException e) {
                throw new TransformerConfigurationException(e);
            }
            TransformerFactory transfakt = TransformerFactory.newInstance();
            transfakt.setURIResolver(MCRURIResolver.instance());
            trans = transfakt.newTransformer(source);
        }
        return trans;
    }

    /**
     * Save all MCRClassifications.
     *
     * @param dirname
     *            the directory name to store all classifications
     * @param style
     *            the name part of the stylesheet like <em>style</em>
     *            -classification.xsl
     * @return false if an error was occured, else true
     */
    @MCRCommand(syntax = "export all classifications to directory {0} with {1}", help = "The command store all classifications to the directory with name {0} with the stylesheet {1}-object.xsl. For {1} save is the default.", order = 70)
    public static boolean exportAll(String dirname, String style) throws Exception {
        List<MCRCategoryID> allClassIds = DAO.getRootCategoryIDs();
        boolean ret = false;
        for (MCRCategoryID id : allClassIds) {
            ret = ret & export(id.getRootID(), dirname, style);
        }
        return ret;
    }

    /**
     * List all IDs of all classifications stored in the database
     */
    @MCRCommand(syntax = "list all classifications", help = "The command list all classification stored in the database.", order = 100)
    public static void listAllClassifications() {
        List<MCRCategoryID> allClassIds = DAO.getRootCategoryIDs();
        for (MCRCategoryID id : allClassIds) {
            LOGGER.info(id.getRootID());
        }
        LOGGER.info("");
    }

    /**
     * List a MCRClassification.
     *
     * @param classid
     *            the MCRObjectID of the classification
     */
    @MCRCommand(syntax = "list classification {0}", help = "The command list the classification with MCRObjectID {0}.", order = 90)
    public static void listClassification(String classid) {
        MCRCategoryID clid = MCRCategoryID.rootID(classid);
        MCRCategory cl = DAO.getCategory(clid, -1);
        LOGGER.info(classid);
        if (cl != null) {
            listCategory(cl);
        } else {
            LOGGER.error("Can't find classification " + classid);
        }
    }

    private static void listCategory(MCRCategory categ) {
        int level = categ.getLevel();
        StringBuilder sb = new StringBuilder(128);
        for (int i = 0; i < level * 2; i++) {
            sb.append(' ');
        }
        String space = sb.toString();
        if (categ.isCategory()) {
            LOGGER.info(space + "  ID    : " + categ.getId().getID());
        }
        for (MCRLabel label : categ.getLabels()) {
            LOGGER.info(space + "  Label : (" + label.getLang() + ") " + label.getText());
        }
        List<MCRCategory> children = categ.getChildren();
        for (MCRCategory child : children) {
            listCategory(child);
        }
    }

    @MCRCommand(syntax = "repair category with empty labels", help = "fixes all categories with no labels (adds a label with categid as @text for default lang)", order = 110)
    public static void repairEmptyLabels() {
        Session session = MCRHIBConnection.instance().getSession();
        String deleteEmptyLabels = "delete from {h-schema}MCRCategoryLabels where text is null or trim(text) = ''";
        int affected = session.createNativeQuery(deleteEmptyLabels).executeUpdate();
        LOGGER.info("Deleted " + affected + " labels.");
        String sqlQuery = "select cat.classid,cat.categid from {h-schema}MCRCategory cat left outer join {h-schema}MCRCategoryLabels label on cat.internalid = label.category where label.text is null";
        @SuppressWarnings("unchecked")
        List<Object[]> list = session.createNativeQuery(sqlQuery).getResultList();

        for (Object resultList : list) {
            Object[] arrayOfResults = (Object[]) resultList;
            String classIDString = (String) arrayOfResults[0];
            String categIDString = (String) arrayOfResults[1];

            MCRCategoryID mcrCategID = new MCRCategoryID(classIDString, categIDString);
            MCRLabel mcrCategLabel = new MCRLabel(MCRConstants.DEFAULT_LANG, categIDString, null);
            MCRCategoryDAOFactory.getInstance().setLabel(mcrCategID, mcrCategLabel);
            LOGGER.info("fixing category with class ID \"" + classIDString + "\" and category ID \"" + categIDString
                    + "\"");
        }
        LOGGER.info("Fixing category labels completed!");
    }

    @MCRCommand(syntax = "repair position in parent", help = "fixes all categories gaps in position in parent", order = 120)
    @SuppressWarnings("unchecked")
    public static void repairPositionInParent() {
        Session session = MCRHIBConnection.instance().getSession();
        // this SQL-query find missing numbers in positioninparent
        String sqlQuery = "select parentid, min(cat1.positioninparent+1) from {h-schema}MCRCategory cat1 "
                + "where cat1.parentid is not null and not exists" + "(select 1 from {h-schema}MCRCategory cat2 "
                + "where cat2.parentid=cat1.parentid and cat2.positioninparent=(cat1.positioninparent+1))"
                + "and cat1.positioninparent not in "
                + "(select max(cat3.positioninparent) from {h-schema}MCRCategory cat3 "
                + "where cat3.parentid=cat1.parentid) group by cat1.parentid";

        for (List<Object[]> parentWithErrorsList = session.createNativeQuery(sqlQuery)
                .getResultList(); !parentWithErrorsList
                        .isEmpty(); parentWithErrorsList = session.createNativeQuery(sqlQuery).getResultList()) {
            for (Object[] parentWithErrors : parentWithErrorsList) {
                Number parentID = (Number) parentWithErrors[0];
                Number firstErrorPositionInParent = (Number) parentWithErrors[1];
                LOGGER.info("Category " + parentID + " has the missing position " + firstErrorPositionInParent
                        + " ...");
                repairCategoryWithGapInPos(parentID, firstErrorPositionInParent);
                LOGGER.info("Fixed position " + firstErrorPositionInParent + " for category " + parentID + ".");
            }
        }

        sqlQuery = "select parentid, min(cat1.positioninparent-1) from {h-schema}MCRCategory cat1 "
                + "where cat1.parentid is not null and not exists" + "(select 1 from {h-schema}MCRCategory cat2 "
                + "where cat2.parentid=cat1.parentid and cat2.positioninparent=(cat1.positioninparent-1))"
                + "and cat1.positioninparent not in "
                + "(select max(cat3.positioninparent) from {h-schema}MCRCategory cat3 "
                + "where cat3.parentid=cat1.parentid) and cat1.positioninparent > 0 group by cat1.parentid";

        while (true) {
            List<Object[]> parentWithErrorsList = session.createNativeQuery(sqlQuery).getResultList();

            if (parentWithErrorsList.isEmpty()) {
                break;
            }

            for (Object[] parentWithErrors : parentWithErrorsList) {
                Number parentID = (Number) parentWithErrors[0];
                Number wrongStartPositionInParent = (Number) parentWithErrors[1];
                LOGGER.info("Category " + parentID + " has the the starting position " + wrongStartPositionInParent
                        + " ...");
                repairCategoryWithWrongStartPos(parentID, wrongStartPositionInParent);
                LOGGER.info("Fixed position " + wrongStartPositionInParent + " for category " + parentID + ".");
            }
        }
        LOGGER.info("Repair position in parent finished!");
    }

    public static void repairCategoryWithWrongStartPos(Number parentID, Number wrongStartPositionInParent) {
        Session session = MCRHIBConnection.instance().getSession();
        String sqlQuery = "update {h-schema}MCRCategory set positioninparent= positioninparent -"
                + wrongStartPositionInParent + "-1 where parentid=" + parentID + " and positioninparent > "
                + wrongStartPositionInParent;

        session.createNativeQuery(sqlQuery).executeUpdate();
    }

    private static void repairCategoryWithGapInPos(Number parentID, Number firstErrorPositionInParent) {
        Session session = MCRHIBConnection.instance().getSession();
        // the query decrease the position in parent with a rate.
        // eg. posInParent: 0 1 2 5 6 7
        // at 3 the position get faulty, 5 is the min. of the position greather
        // 3
        // so the reate is 5-3 = 2
        String sqlQuery = "update {h-schema}MCRCategory set positioninparent=(positioninparent - (select min(positioninparent) from {h-schema}MCRCategory where parentid="
                + parentID + " and positioninparent > " + firstErrorPositionInParent + ")+"
                + firstErrorPositionInParent + ") where parentid=" + parentID + " and positioninparent > "
                + firstErrorPositionInParent;

        session.createNativeQuery(sqlQuery).executeUpdate();
    }

    @MCRCommand(syntax = "repair left right values for classification {0}", help = "fixes all left and right values in the given classification", order = 130)
    public static void repairLeftRightValue(String classID) {
        if (!(DAO instanceof MCRCategoryDAOImpl)) {
            LOGGER.error("Command not compatible with " + DAO.getClass().getName());
            return;
        }
        ((MCRCategoryDAOImpl) DAO).repairLeftRightValue(classID);
    }

    @MCRCommand(syntax = "check all classifications", help = "checks if all redundant information are stored without conflicts", order = 140)
    public static List<String> checkAllClassifications() {
        List<MCRCategoryID> classifications = MCRCategoryDAOFactory.getInstance().getRootCategoryIDs();
        List<String> commands = new ArrayList<String>(classifications.size());
        for (MCRCategoryID id : classifications) {
            commands.add("check classification " + id.getRootID());
        }
        return commands;
    }

    @MCRCommand(syntax = "check classification {0}", help = "checks if all redundant information are stored without conflicts", order = 150)
    public static void checkClassification(String id) {
        LOGGER.info("Checking classifcation " + id);
        ArrayList<String> log = new ArrayList<String>();
        LOGGER.info(id + ": checking for missing parentID");
        checkMissingParent(id, log);
        LOGGER.info(id + ": checking for empty labels");
        checkEmptyLabels(id, log);
        if (log.isEmpty()) {
            MCRCategoryImpl category = (MCRCategoryImpl) MCRCategoryDAOFactory.getInstance()
                    .getCategory(MCRCategoryID.rootID(id), -1);
            LOGGER.info(id + ": checking left, right and level values and for non-null children");
            checkLeftRightAndLevel(category, 0, 0, log);
        }
        if (log.size() > 0) {
            LOGGER.error("Some errors occured on last test, report will follow");
            StringBuilder sb = new StringBuilder();
            for (String msg : log) {
                sb.append(msg).append('\n');
            }
            LOGGER.error("Error report for classification " + id + "\n" + sb);
        } else {
            LOGGER.info("Classifcation " + id + " has no errors.");
        }
    }

    private static int checkLeftRightAndLevel(MCRCategoryImpl category, int leftStart, int levelStart,
            List<String> log) {
        int curValue = leftStart;
        final int nextLevel = levelStart + 1;
        if (leftStart != category.getLeft())
            log.add("LEFT of " + category.getId() + " is " + category.getLeft() + " should be " + leftStart);
        if (levelStart != category.getLevel())
            log.add("LEVEL of " + category.getId() + " is " + category.getLevel() + " should be " + levelStart);
        int position = 0;
        for (MCRCategory child : category.getChildren()) {
            if (child == null) {
                log.add("NULL child of parent " + category.getId() + " on position " + position);
                continue;
            }
            LOGGER.debug(child.getId());
            curValue = checkLeftRightAndLevel((MCRCategoryImpl) child, ++curValue, nextLevel, log);
            position++;
        }
        ++curValue;
        if (curValue != category.getRight())
            log.add("RIGHT of " + category.getId() + " is " + category.getRight() + " should be " + curValue);
        return curValue;
    }

    private static void checkEmptyLabels(String classID, List<String> log) {
        Session session = MCRHIBConnection.instance().getSession();
        String sqlQuery = "select cat.categid from {h-schema}MCRCategory cat left outer join {h-schema}MCRCategoryLabels label on cat.internalid = label.category where cat.classid='"
                + classID + "' and (label.text is null or trim(label.text) = '')";
        @SuppressWarnings("unchecked")
        List<String> list = session.createNativeQuery(sqlQuery).getResultList();

        for (String categIDString : list) {
            log.add("EMPTY lables for category " + new MCRCategoryID(classID, categIDString));
        }
    }

    private static void checkMissingParent(String classID, List<String> log) {
        Session session = MCRHIBConnection.instance().getSession();
        String sqlQuery = "select cat.categid from {h-schema}MCRCategory cat WHERE cat.classid='" + classID
                + "' and cat.level > 0 and cat.parentID is NULL";
        @SuppressWarnings("unchecked")
        List<String> list = session.createNativeQuery(sqlQuery).getResultList();

        for (String categIDString : list) {
            log.add("parentID is null for category " + new MCRCategoryID(classID, categIDString));
        }
    }

    @MCRCommand(syntax = "repair missing parent for classification {0}", help = "restores parentID from information in the given classification, if left right values are correct", order = 130)
    public static void repairMissingParent(String classID) {
        Session session = MCRHIBConnection.instance().getSession();
        String sqlQuery = "update {h-schema}MCRCategory cat set cat.parentID=(select parent.internalID from {h-schema}MCRCategory parent WHERE parent.classid='"
                + classID
                + "' and parent.leftValue<cat.leftValue and parent.rightValue>cat.rightValue and parent.level=(cat.level-1)) WHERE cat.classid='"
                + classID + "' and cat.level > 0 and cat.parentID is NULL";
        int updates = session.createNativeQuery(sqlQuery).executeUpdate();
        LOGGER.info(() -> "Repaired " + updates + " parentID columns for classification " + classID);
    }
}