org.rolap4j.parsers.Dom4jBasedCatalogParser.java Source code

Java tutorial

Introduction

Here is the source code for org.rolap4j.parsers.Dom4jBasedCatalogParser.java

Source

/*
 * Licensed to Rolap4J Framework under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Rolap4J Framework licenses this file to you under
 * the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.rolap4j.parsers;

import lombok.extern.slf4j.Slf4j;
import org.dom4j.Document;
import org.dom4j.Node;
import org.rolap4j.common.*;
import org.rolap4j.exceptions.FileNotFoundException;
import org.rolap4j.exceptions.ParsingException;
import org.rolap4j.helpers.ElementPropertyValueAvailabilityChecker;
import org.rolap4j.utils.PropertyUtil;
import org.rolap4j.utils.StringUtil;
import org.rolap4j.utils.XmlDocumentUtil;

import java.util.*;

/**
 * <p>This is an implementation of {@link CatalogParser} that parses the catalog (mapping file) that describes
 * the data-warehouse structure.This implementation is <a href="">Doma4j</a>  based parser.</p>
 *
 * @author <a href="mailto:contact@andriantomanga.com">Nabil Andriantomanga</a>
 * @version 1.0-RELEASE
 * @see CatalogParser
 * @since 1.0-RELEASE
 */
@Slf4j
public class Dom4jBasedCatalogParser extends AbstractCatalogParser {

    /**
     * @param catalogFilePath path to the catalog file (please see the documention)
     */
    public Dom4jBasedCatalogParser(String catalogFilePath) {
        super(catalogFilePath);
    }

    /**
     * {@inheritDoc}. <br>
     * The example bellow shows how to get an instance of {@link Schema} from an existing catalog : <br>
     * <pre>
     *
     *     {@code
     *          CatalogParser parser = new Dom4jBasedCatalogParser("my-data-warehouse-mapping.xml");
     *          // can use another implementation
     *          try {
     *              Schema schema = parser.parseCatalog();
     *              // use schema to reach cubes, dimensions, hierarchies, levels ...
     *          } catch(Rolap4jException eX) {
     *              eX.printStackTrace();
     *          }
     *     }
     * </pre>
     *
     * @return A {@link Schema} corresponding to the filePath
     * @throws FileNotFoundException The specified file was not found
     * @throws ParsingException      An error occurred when parsing the file
     */
    @Override
    public Schema parseCatalog() throws FileNotFoundException, ParsingException {

        final Document document = createDocument();

        Node schemaNode = document.selectSingleNode("//Schema");
        if (null == schemaNode) {
            throw new ParsingException("The root element (Schema) was not found.");
        }

        final String schemaName = schemaNode.valueOf("@name");
        final Schema schema = new Schema();
        schema.setName(StringUtil.isEmpty(schemaName) ? Schema.DEFAULT_NAME : schemaName);

        // Loading dimensions ...
        schema.addDimensions(loadDimensions(document));

        // Loading cubes ...
        schema.addCubes(loadCubes(document, schema));

        return schema;
    } // end parseCatalog( ...

    /**
     * Get all cubes mapped in the data-warehouse mapping file
     *
     * @param document An instance of {@link Document}
     * @return
     * @see Cube
     */
    private List<Cube> loadCubes(final Document document, Schema schema) throws ParsingException {

        log.debug("Loading all cubes from the schema ...");
        List<Cube> cubes = new ArrayList<>();

        List<Node> cubeNodes = document.selectNodes("//Schema/Cube");
        Set<String> notAvailableCubeNames = new HashSet<>();
        String worker;
        int sprinter = 1;
        for (Node cubeNode : cubeNodes) {

            // Set the cube name
            worker = cubeNode.valueOf("@name");
            if (StringUtil.isEmpty(worker)) {
                throw new ParsingException("The cube number " + sprinter + " must have a name attribute defined");
            }
            checkCubeAvailability(worker, notAvailableCubeNames);
            final Cube cube = new Cube();
            cube.setName(worker);

            // Set the table name
            cube.setTableName(getCubeTableName(cubeNode, cube.getName()));

            // Set dimensions
            cube.addDimensions(getDimensions(cubeNode, schema));

            // Set measures
            cube.addMeasures(getMeasures(cubeNode, schema));

            cubes.add(cube);

            log.debug("\t{} cube was found", cube.getName());
            sprinter++;
        }
        return cubes;
    }

    /**
     * @param cubeNode
     * @param schema
     * @return
     * @throws ParsingException
     */
    private Collection<Measure> getMeasures(Node cubeNode, Schema schema) throws ParsingException {

        List<Measure> measures = new ArrayList<>();
        List<Node> measureNodes = cubeNode.selectNodes("Measure");
        String worker;
        for (Node measureNode : measureNodes) {

            worker = measureNode.valueOf("@name");
            if (StringUtil.isEmpty(worker)) {
                throw new ParsingException("The name of the measure attribute must be specified");
            }
            Measure measure = new Measure();
            measure.setName(worker);
            measures.add(measure);
        }
        return measures;
    }

    /**
     * @param cubeNode
     * @param schema
     * @return
     * @throws ParsingException
     */
    private Collection<Dimension> getDimensions(Node cubeNode, final Schema schema) throws ParsingException {

        List<Dimension> dimensions = new ArrayList<>();
        List<Node> dimensionNodes = cubeNode.selectNodes("DimensionUsage");
        String worker;

        for (Node dimensionNode : dimensionNodes) {

            worker = dimensionNode.valueOf("@name");
            if (StringUtil.isEmpty(worker)) {
                throw new ParsingException("The name of the dimensionUsage attribute must be specified");
            }

            if (!schema.containsDimension(worker)) {
                StringBuilder names = new StringBuilder();
                for (Iterator<String> iter = schema.getDimensions().keySet().iterator(); iter.hasNext();) {
                    names.append(iter.next()).append(", ");
                }
                if (names.length() > 0) {
                    names.setLength(names.length() - 1);
                }
                throw new ParsingException("Unexpected dimension found : " + worker
                        + " when one of the following dimensions is expected : " + names);

            }
            dimensions.add(schema.getDimension(worker));
        }
        return dimensions;
    }

    /**
     * Get the table name that corresponds to the cube
     *
     * @param cubeNode
     * @param cubeName
     * @return table name that corresponds to the cube
     * @throws ParsingException
     * @see Cube
     */
    private String getCubeTableName(final Node cubeNode, final String cubeName) throws ParsingException {

        final Node tableNode = cubeNode.selectSingleNode("Table");
        final String exceptionMessage = "The cube " + cubeName + " must specify a corresponding table";

        if (null == tableNode) {
            throw new ParsingException(exceptionMessage);
        }

        final String name = tableNode.valueOf("@name");
        if (StringUtil.isEmpty(name)) {
            throw new ParsingException(exceptionMessage);
        }
        return name;
    }

    /**
     * Check if the cube name is still available
     *
     * @param cubeName
     * @param notAvailableCubeNames
     * @throws ParsingException
     */
    private void checkCubeAvailability(final String cubeName, Set<String> notAvailableCubeNames)
            throws ParsingException {

        if (!ElementPropertyValueAvailabilityChecker.isAvailable(cubeName, notAvailableCubeNames, ElementType.CUBE,
                "name")) {
            throw new ParsingException("The cube name must be unique");
        }
    }

    /**
     * Get all dimensions mapped in the data-warehouse mapping file
     *
     * @param document An instance of {@link Document}
     * @return All dimensions
     * @throws ParsingException
     * @see Cube
     */
    private List<Dimension> loadDimensions(final Document document) throws ParsingException {

        log.debug("Loading all dimensions from the schema ...");
        List<Dimension> dimensions = new ArrayList<>();

        List<Node> dimensionNodes = document.selectNodes("//Schema/Dimension");
        Set<String> notAvailableDimensionNames = new HashSet<>();
        int sprinter = 1;
        String dimensionName;

        for (Node node : dimensionNodes) {

            dimensionName = node.valueOf("@name");
            if (StringUtil.isEmpty(dimensionName)) {
                throw new ParsingException("A dimension name is missing. The dimension index is : " + sprinter);
            }

            checkDimensionAvailability(dimensionName, notAvailableDimensionNames);
            final Dimension dimension = new Dimension();
            dimension.setName(dimensionName);
            dimension.addHierarchies(loadHierarchies(node, dimensionName));
            dimensions.add(dimension);

            log.debug("\t{} dimension was found", dimension.getName());
            sprinter++;
        }

        log.debug("{} dimension(s) successfully loaded", (sprinter - 1));
        return dimensions;
    } // end loadDimensions( ...

    /**
     * Load the current dimension hierachies
     *
     * @param dimensionNode
     * @param dimensionName
     * @return list of hierarchies that correspond to the dimension
     * @throws ParsingException an error occurred when parsing the file
     */
    private List<Hierarchy> loadHierarchies(final Node dimensionNode, final String dimensionName)
            throws ParsingException {

        log.debug("Loading all {} hierarchies", dimensionName);
        List<Node> hierarchiesNodes = dimensionNode.selectNodes("Hierarchy");
        List<Hierarchy> hierarchies = new ArrayList<>();
        Set<String> notAvailableHierarchyNames = new HashSet<>();
        String worker;
        int sprinter = 1;
        boolean defaultHierarchyDefined = false;

        for (Node hierarchyNode : hierarchiesNodes) {

            worker = hierarchyNode.valueOf("@hasAll");
            checkHasAllValidity(worker, dimensionName, sprinter);
            Hierarchy hierarchy = new Hierarchy();
            hierarchy.setHasAll("true".equalsIgnoreCase(worker));

            // get the required name ...
            worker = hierarchyNode.valueOf("@name");
            if (StringUtil.isEmpty(worker) && !defaultHierarchyDefined) {
                throw new ParsingException("An hierarchy name is missing. The hierarchy index is : " + sprinter);
            }
            defaultHierarchyDefined = true;

            // set the name if available ...
            checkHiearchyAvailability(worker, notAvailableHierarchyNames);
            hierarchy.setName(worker);

            // set the hierarchy primary key attribute ...
            worker = hierarchyNode.valueOf("@primaryKey");
            if (StringUtil.isEmpty(worker)) {
                throw new ParsingException("The following attribute must be specified for the " + dimensionName
                        + " dimension : primaryKey");
            }
            hierarchy.setPrimaryKey(worker);
            hierarchy.addLevels(loadLevels(hierarchyNode, hierarchy.getName(), dimensionName));

            hierarchies.add(hierarchy);
            log.debug("\tHierarchy {} added to {} hierarchies", hierarchy.getName(), dimensionName);
        }

        return hierarchies;
    } // end loadHierarchies( ...

    /**
     * Load all levels from an hierarchy node
     *
     * @param hierarchyNode The hierarchy node
     * @param hierarchyName The name of the concerned hierarchy
     * @param dimensionName The name of the concerned dimension
     * @return All the levels of the specified hierarchy
     * @throws ParsingException
     * @see Level
     */
    private List<Level> loadLevels(final Node hierarchyNode, final String hierarchyName, final String dimensionName)
            throws ParsingException {

        log.debug("Loading all {} levels ...", hierarchyName);
        List<Level> levels = new ArrayList<>();
        final List<Node> levelNodes = hierarchyNode.selectNodes("Level");
        String worker;
        int sprinter = 1;

        for (Node levelNode : levelNodes) {

            worker = levelNode.valueOf("@name");
            if (StringUtil.isEmpty(worker)) {

                StringBuilder exceptionMessage = new StringBuilder();
                exceptionMessage.append("The name attribute is missing for the #").append(sprinter)
                        .append(" level of the ").append(hierarchyName).append(" hierarchy of the ")
                        .append(dimensionName).append(" dimension ");

                throw new ParsingException(exceptionMessage.toString());
            }
            Level level = new Level();
            level.setName(worker);

            worker = levelNode.valueOf("@column");
            if (StringUtil.isEmpty(worker)) {
                throw new ParsingException("A column attribute is required for the " + level.getName());
            }
            level.setColumn(worker);

            worker = levelNode.valueOf("@uniqueMembers");
            checkUniqueMembersValidity(worker, level.getName());
            level.setUniqueMembers("true".equalsIgnoreCase(worker));

            level.addProperties(loadProperties(levelNode, level.getName(), dimensionName));
            log.debug("\tLevel {} added to {} hierarchy", level.getName(), hierarchyName);
            sprinter++;
        }
        return levels;

    } // end loadLevels( ...

    /**
     * @param levelNode
     * @param levelName
     * @param dimensionName
     * @return
     * @throws ParsingException
     */
    private Set<Property> loadProperties(final Node levelNode, final String levelName, final String dimensionName)
            throws ParsingException {

        log.debug("Loading all {} level properties ...", levelName);
        Set<Property> properties = new HashSet<>();
        List<Node> propertyNodes = levelNode.selectNodes("Property");
        String worker;
        Set<String> notAvailablePropertyNames = new HashSet<>();

        for (Node propertyNode : propertyNodes) {

            worker = propertyNode.valueOf("@name");

            if (StringUtil.isEmpty(worker)) {
                throw new ParsingException(
                        "The name attribute is missing for one or some properties of " + levelName + " level");
            }

            checkPropertyAvailability(worker, notAvailablePropertyNames);
            final Property property = new Property();
            property.setName(worker);
            worker = propertyNode.valueOf("@column");

            if (StringUtil.isEmpty(worker)) {
                throw new ParsingException("A column attribute is required for " + property.getName()
                        + " property of " + levelName + " level");
            }

            property.setColumn(worker);
            property.setType(PropertyUtil.getPropertyType(propertyNode.valueOf("@type")));
            properties.add(property);
        }
        return properties;
    } // loadProperties( ...

    /**
     * Check if the property name is still available
     *
     * @param propertyName
     * @param notAvailablePropertyNames
     * @throws ParsingException
     */
    private void checkPropertyAvailability(final String propertyName, Set<String> notAvailablePropertyNames)
            throws ParsingException {

        if (!ElementPropertyValueAvailabilityChecker.isAvailable(propertyName, notAvailablePropertyNames,
                ElementType.PROPERTY, "name")) {
            throw new ParsingException("The property name must be unique");
        }

    }

    /**
     * @param uniqueMembersValue
     * @param levelName
     * @throws ParsingException
     */
    private void checkUniqueMembersValidity(String uniqueMembersValue, final String levelName)
            throws ParsingException {

        if (StringUtil.isEmpty(uniqueMembersValue)) {
            throw new ParsingException("The uniqueMembers attribute is required for the " + levelName + " level");
        }

        if ("true".equalsIgnoreCase(uniqueMembersValue) || "false".equalsIgnoreCase(uniqueMembersValue)) {
            return; // valid hasAll property ...
        }

        throw new ParsingException(
                "The value of the uniqueMembers attribute must be one of the following : 'true' or 'false'");
    }

    /**
     * Check if the cube hierarchy is still available
     *
     * @param hierarchyName
     * @param notAvailableHierarchyNames
     * @throws ParsingException
     */
    private void checkHiearchyAvailability(final String hierarchyName, Set<String> notAvailableHierarchyNames)
            throws ParsingException {

        if (!ElementPropertyValueAvailabilityChecker.isAvailable(hierarchyName, notAvailableHierarchyNames,
                ElementType.HIERARCHY, "name")) {
            throw new ParsingException("The hierarchy name must be unique");
        }
    }

    /**
     * Check if the 'hasAll' property is valid
     *
     * @param hasAll        value to be checked
     * @param dimensionName concerned dimension name
     * @param index         index of the dimension in the schema
     * @throws ParsingException an error occurred when parsing the file
     */
    private void checkHasAllValidity(final String hasAll, final String dimensionName, int index)
            throws ParsingException {

        if (StringUtil.isEmpty(hasAll)) {
            // log before throwing the exception ...
            log.error("The hasAll property is required for the {} dimension", dimensionName);

            throw new ParsingException(
                    "The hasAll property is required for the " + dimensionName + " dimension. #" + index);
        }

        if ("true".equalsIgnoreCase(hasAll) || "false".equalsIgnoreCase(hasAll)) {
            return; // valid hasAll property ...
        }

        final StringBuilder exceptionMessage = new StringBuilder();
        exceptionMessage.append("Expected value of the 'hasAll' property of ").append(dimensionName)
                .append(" element is one of the following :");
        exceptionMessage.append(" (true, false), but the following value was found : ").append(hasAll);

        throw new ParsingException(exceptionMessage.toString());
    }

    /**
     * @return an instance of {@link Document} corresponding to the file to be parsed
     * @throws FileNotFoundException The file to be parsed is not found
     * @throws ParsingException      An error occured when parsing the file
     */
    private Document createDocument() throws FileNotFoundException, ParsingException {

        return XmlDocumentUtil.createDocumentFromXml(this.catalogFilePath);
    }

    /**
     * Check if the dimension name is still available
     *
     * @param dimensionName
     * @param notAvailableDimensionNames
     * @throws ParsingException
     */
    private void checkDimensionAvailability(String dimensionName, Set<String> notAvailableDimensionNames)
            throws ParsingException {

        if (!ElementPropertyValueAvailabilityChecker.isAvailable(dimensionName, notAvailableDimensionNames,
                ElementType.DIMENSION, "name")) {
            throw new ParsingException("The dimension name must be unique");
        }

    }
}