sernet.verinice.report.service.impl.dynamictable.GraphReportDataModel.java Source code

Java tutorial

Introduction

Here is the source code for sernet.verinice.report.service.impl.dynamictable.GraphReportDataModel.java

Source

/*******************************************************************************
 * Copyright (c) 2015 Sebastian Hagedorn.
 *
 * This program is free software: you can redistribute it and/or 
 * modify it under the terms of the GNU Lesser General Public License 
 * as published by the Free Software Foundation, either version 3 
 * 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program. 
 * If not, see <http://www.gnu.org/licenses/>.
 * 
 * Contributors:
 *     Sebastian Hagedorn <sh[at]sernet[dot]de> - initial API and implementation
 ******************************************************************************/
package sernet.verinice.report.service.impl.dynamictable;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

import sernet.gs.service.NumericStringComparator;
import sernet.hui.common.connect.EntityType;
import sernet.hui.common.connect.HUITypeFactory;
import sernet.hui.common.connect.PropertyType;
import sernet.hui.common.connect.URLUtil;
import sernet.verinice.interfaces.CommandException;
import sernet.verinice.interfaces.graph.VeriniceGraph;
import sernet.verinice.model.common.CnATreeElement;

/**
 * This data model simplifies the user request to implement a report template that
 * displays a table over all elements of type $a, and all to that element linked elements of 
 * type $b. <br>
 * The data model should be used in verinice reports only, usage (in a dataset) should look like this:
 * <pre>
 * ============================================================================================
 * command = new GraphCommand();
 * loader = new GraphElementLoader();
 * loader.setScopeId(root);
 * loader.setTypeIds(new String[]{ Asset.TYPE_ID, IncidentScenario.TYPE_ID});
 * command.addLoader(loader);
 * command.addRelationId(IncidentScenario.REL_INCSCEN_ASSET);
 * command.addRelationId("rel_person_incscen_modl");
 * command = helper.execute(command);          
 * graph = command.getGraph();
    
 * dm = new GraphReportDataModel(graph, new String[]{"asset/asset_name", "asset/incident_scenario/incident_scenario_name"});
 * dm.init();
 * return dm.getResults(); 
 * ============================================================================================
 * </pre>
 * 
 * it returns a {@link List<List<String>}, so that a standard report table could be filled with that data
 * 
 * Syntax for Strings passed to constructor:<br>
 * Strings are always constructed as a kind of path over different entitytypes and ending 
 * with a property type. Entitytypes could be separated with 4 different operators:
 * <pre>
 *  - LINK_TYPE_DELIMITER = '/'
 *      this separates two entitytypes that are linked to each other AND a entitytype and the
 *      property which should be put into the table
 *      e.g.: asset/asset_name, asset/incident_scenario/incident_scenario_name
 *  - CHILD_TYPE_DELIMITER = '>'
 *      this separates two entitytypes that are in a parent>child relation
 *       e.g.: baustein-umsetzung>massnahmen-umsetzung/mnums_name
 *  - PARENT_TYPE_DELIMITER = '<'
 *      this separates two entitytypes that are in a child<parent relation
 *      e.g.: massnahmen-umsetzung<baustein-umsetzung/bstumsetzung_name
 *  - END_OF_PATH_DELIMITER = '#'
 *      this is for internal use only, to mark the end of a propertypath, please do not use this manually
 * </pre>
 * 
 * class is marked deprecated, please use {@link GenericDataModel} only
 * 
 * @author Sebastian Hagedorn <sh[at]sernet[dot]de>
 * @author Daniel Murygin <dm[at]sernet[dot]de>
 */
@Deprecated
public class GraphReportDataModel {

    private static final Logger LOG = Logger.getLogger(GraphReportDataModel.class);

    private VeriniceGraph graph;

    private String[] userColumnStrings;

    private List<List<String>> table;

    private static final char LINK_TYPE_DELIMITER = '/';
    private static final char CHILD_TYPE_DELIMITER = '>';
    private static final char PARENT_TYPE_DELIMITER = '<';
    private static final char END_OF_PATH_DELIMITER = '#';

    private static final String EMPTY_PROPERTY = "empty_property";

    private static final NumericStringComparator NSC = new NumericStringComparator();

    String propertyPath;

    /**
     * user generated input string are being parsed into two categories, operators and operands,
     * and beeing stored on stacks (in reverse order)
     */
    private Stack<String> operandStack;
    private Stack<String> operatorStack;
    private String operand;
    private String operator;

    private Set<CnATreeElement> elementSet;
    private CnATreeElement parentElement;
    private CnATreeElement element;

    // used for storing temporary results and final result generation
    private Map<String, TableRow> resultMap;

    // graph must be created within report template as shown above in class comment
    public GraphReportDataModel(VeriniceGraph graph, String[] columns) {
        this.graph = graph;
        this.userColumnStrings = (columns != null) ? columns.clone() : null;
        this.resultMap = new HashMap<String, TableRow>();
        this.operandStack = new Stack<String>();
        this.operatorStack = new Stack<String>();
    }

    public void init() {
        try {
            doInit();
        } catch (Exception e) {
            LOG.error("Error while creating data model", e);
        }
    }

    private void doInit() throws CommandException {
        // iterate over all userStrings
        for (int i = 0; i < userColumnStrings.length; i++) {
            propertyPath = userColumnStrings[i];
            if (LOG.isInfoEnabled()) {
                LOG.info("Inspecting propertyPath:\t" + propertyPath + "\n\n\n");
            }
            // clear stack for new string
            clearStacks();
            // fill stacks with new input
            fillStacks(reversePropertyPath(propertyPath));
            if (operatorStack.size() != operandStack.size()) {
                LOG.warn("wrong stacksizes detected. " + operandStack.size() + " operands and "
                        + operatorStack.size()
                        + " operators. Should always be equal (including termination operator, added automatically)");
            }
            elementSet = graph.getElements(operandStack.peek());
            parentElement = null;
            if (LOG.isDebugEnabled()) {
                LOG.debug("first round, " + elementSet.size() + " elements of type:\t" + operand);
            }
            createColumn("", i);
        }
    }

    /**
     * Iterate over a set of elements which are loaded from the {@link VeriniceGraph}, 
     * which is initialized within a report dataset. method iterates a single propertyPath at first
     * and linked elements at second to navigate to a given property and add that to a list, 
     * that represent a row in the report table 
     * 
     * @param rowId - identifier of root element for next row, used for storing row in hashmap
     * @param propertyPosition - position of property in result row  
     */
    private void createColumn(String rowId, int propertyPosition) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Now iterating " + elementSet.size() + " elements");
        }
        for (CnATreeElement elementFromSet : elementSet) { // handle every element of current set (data loaded from graph)
            this.element = elementFromSet;
            handleElement(rowId, propertyPosition);
        }
    }

    private void handleElement(String rowId, int propertyPosition) {
        String newRowId = new StringBuilder().append(rowId).append("#").append(element.getDbId()).toString();
        operator = operatorStack.pop();
        operand = operandStack.pop();
        String currentOperator = operator;
        String currentOperand = operand;
        logElement(rowId, newRowId);
        char operatorChar = operator.toCharArray()[0];
        if (LINK_TYPE_DELIMITER == operatorChar) {
            parentElement = handleLinkOperator(element.getTypeId(), newRowId, propertyPosition);
        } else if (CHILD_TYPE_DELIMITER == operatorChar || PARENT_TYPE_DELIMITER == operatorChar) {
            handleParentChildOperator(newRowId, propertyPosition, operatorChar);
        } else if (END_OF_PATH_DELIMITER == operatorChar) {
            LOG.error("something went wrong here, point should not be reached");
        } else if (LOG.isDebugEnabled()) {
            LOG.debug("used operator is not support, please contact your support or read the api - documenation");
        }
        operandStack.push(currentOperand);
        operatorStack.push(currentOperator);
    }

    /**
     * Load children or parent element(s) (depends on operator) of type nextEntityType and 
     * passes them to back to doOneStep()
     * 
     * @param rowId
     * @param propertyPosition
     * @param operator
     */
    private void handleParentChildOperator(String rowId, int propertyPosition, char operatorChar) {
        operand = operandStack.peek();
        elementSet = getElementsFromGraph(operatorChar, operand);
        parentElement = element;
        operator = operatorStack.peek();
        createColumn(rowId, propertyPosition);
    }

    /**
     * Load linked elements of type nextEntityType and passes them to back to doOneStep()
     * 
     * @param typeId
     * @param rowId
     * @param propertyPosition
     * @return
     */
    private CnATreeElement handleLinkOperator(String typeId, String rowId, int propertyPosition) {
        if (!(operandStack.isEmpty()) && isPropertyIdOfTypeId(operandStack.peek(), typeId)) {
            parentElement = handlePropertyOperand(rowId, propertyPosition);
        } else if (!(operandStack.isEmpty()) && isEntityType(operandStack.peek())) {
            elementSet = getElementsFromGraph(operator.toCharArray()[0], operandStack.peek());
            parentElement = element;
            createColumn(rowId, propertyPosition);
        }
        if (operandStack.isEmpty()) {
            // in case of no linked element / no property for given positon and element just add "" instead of null
            if (resultMap.containsKey(rowId)) {
                TableRow row = resultMap.get(rowId);
                if (row.getProperty(propertyPosition) == null) {
                    row.addProperty("", propertyPosition);
                }
            }
        }
        return parentElement;
    }

    /**
     * Reads a property of a given element, computes the row identifier, 
     * loads (if existant) row from result hashmap, and adds property to row 
     * (which is instantiated if non-existant)
     * 
     * @param rowId
     * @param propertyPosition
     * @return
     */
    private CnATreeElement handlePropertyOperand(String rowId, int propertyPosition) {
        String existingPath = rowId;

        if (!resultMap.containsKey(existingPath)) {
            // check recursive upwards if id is already in map
            if (StringUtils.countMatches(rowId, "#") != 1 && rowId.startsWith("#")) {
                existingPath = rowId.substring(0, rowId.lastIndexOf('#'));
            }
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug("Searching for " + existingPath + " on map");
        }
        if (resultMap.containsKey(existingPath)) {
            createSubRow(rowId, propertyPosition, existingPath);
        } else {
            parentElement = createNewRootRow(rowId, operandStack.peek(), propertyPosition);
        }
        return parentElement;
    }

    /**
     * Creates a new {@link TableRow} with a given rootElement and a subelement element.
     * Creates a row to an element which is a sub(linked) element to a given one.
     * 
     * @param rowId
     * @param propertyPosition
     * @param parentRowId
     */
    private void createSubRow(String rowId, int propertyPosition, String parentRowId) {
        TableRow oldRow = resultMap.get(parentRowId);
        TableRow newRow = new TableRow(element.getDbId(), userColumnStrings.length, rowId);
        newRow.setProperties(oldRow.getPropertyList().toArray(new String[oldRow.getPropertyList().size()]));
        if (LOG.isDebugEnabled()) {
            LOG.debug("used \"Oldrow\":\t" + oldRow.toString());
            LOG.debug("Row before insert:\t" + newRow.toString());
        }
        String propertyId = operandStack.peek();
        if (newRow.getProperty(propertyPosition).equals("")) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Inserting property at position " + propertyPosition);
            }
            String value;
            if (!(rowId.equals(EMPTY_PROPERTY))) {
                value = getPropertyValue(propertyId);
            } else {
                value = "";
            }

            newRow.addProperty(value, propertyPosition);
        } else {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Appending Property");
            }
            newRow.addProperty(getPropertyValue(propertyId));
        }
        resultMap.put(rowId, newRow);
        if (LOG.isDebugEnabled()) {
            LOG.debug("Added following row to map:\t" + newRow.toString());
        }
    }

    /**
     * Creates a new table row for a given {@link CnATreeElement} rootElement.
     * 
     * @param rowId
     * @param element
     * @return
     */
    private CnATreeElement createNewRootRow(String rowId, String propertyId, int propertyPosition) {
        if (parentElement == null) {
            parentElement = element;
        }
        TableRow row = new TableRow(parentElement.getDbId(), userColumnStrings.length, rowId);
        String value = "";
        if (!(propertyId.equals(EMPTY_PROPERTY))) {
            value = getPropertyValue(propertyId);
        } else {
            value = "";
        }
        if (value == null) {
            value = "";
        }
        row.addProperty(value, propertyPosition);
        if (LOG.isDebugEnabled()) {
            LOG.debug("Added following row to map:\t" + row.toString());
        }
        resultMap.put(rowId, row);
        return parentElement;
    }

    private String getPropertyValue(String propertyId) {
        String value = element.getEntity().getSimpleValue(propertyId);
        PropertyType propertyType = getPropertyType(propertyId);
        if (propertyType.isURL()) {
            value = URLUtil.getHref(value);
        }
        return value;
    }

    private PropertyType getPropertyType(String propertyId) {
        return getEntityType().getPropertyType(propertyId);
    }

    private EntityType getEntityType() {
        return HUITypeFactory.getInstance().getEntityType(element.getTypeId());
    }

    /**
     * Reverses a string, based on operators.
     * e.g:
     * input: entityType1/entityType2>entityType3/property
     * output: property/entityType3>entityType2/entityType1
     * 
     * @param path
     * @return
     */
    private String reversePropertyPath(String path) {
        StringBuilder revStr = new StringBuilder("");
        int end = path.length(); // substring takes the end index -1
        int counter = path.length() - 1;
        for (int i = path.length() - 1; i >= 0; i--) {
            if (path.charAt(i) == '/' || path.charAt(i) == '<' || path.charAt(i) == '>' || path.charAt(i) == '#'
                    || i == 0) {
                if (i != 0) {
                    revStr.append(path.substring(i + 1, end));
                    revStr.append(path.charAt(i));
                } else {
                    revStr.append(path.substring(i, end));
                }
                end = counter;
            }
            counter--;
        }
        return revStr.toString();
    }

    /**
     * Returns if propertyId is an existing property Id of the entity referenced by typeId.
     * 
     * @param propertyId
     * @param typeId
     * @return
     */
    private boolean isPropertyIdOfTypeId(String propertyId, String typeId) {
        if (propertyId != null && typeId != null) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Checking if <" + propertyId + "> is property of: " + typeId);
            }
            return Arrays.asList(HUITypeFactory.getInstance().getEntityType(typeId).getAllPropertyTypeIds())
                    .contains(propertyId);
        }
        return false;
    }

    /**
     * Loads elements of a given type, 
     * relation and element using an instance of {@link VeriniceGraph}.
     * 
     * @param element
     * @param operator
     * @param typeId
     * @return
     */
    private Set<CnATreeElement> getElementsFromGraph(char operator, String typeId) {
        Set<CnATreeElement> resultSet = new HashSet<CnATreeElement>(0);
        switch (operator) {
        case CHILD_TYPE_DELIMITER:
            resultSet = graph.getChildren(element);
            break;
        case PARENT_TYPE_DELIMITER:
            resultSet.add(graph.getParent(element));
            break;
        case LINK_TYPE_DELIMITER:
            resultSet = graph.getLinkTargetsByElementType(element, typeId);
            break;
        }
        if (LOG.isDebugEnabled()) {
            LOG.debug("returning " + resultSet.size() + " from graph, loaded for element " + element.getTitle()
                    + "<" + element.getDbId() + "> and operator <" + operator + "> and typeId <" + typeId + ">");
        }
        if (resultSet.size() == 0) {
            resultSet.hashCode();
        }
        return resultSet;

    }

    /**
     * Checks if given string is a (in SNCA.xml) defined entity type
     * 
     * @param entityTypeId
     * @return
     */
    private boolean isEntityType(String entityTypeId) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Checking if " + entityTypeId + " is a valid entityTypeId");
        }
        for (EntityType entityType : HUITypeFactory.getInstance().getAllEntityTypes()) {
            if (entityType.getId().equals(entityTypeId)) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug(entityTypeId + " is a valid entityTypeId");
                }
                return true;
            }
        }
        if (LOG.isDebugEnabled()) {
            LOG.debug(entityTypeId + " is NOT a valid entityTypeId");
        }
        return false;
    }

    /**
     * Main method to parse user given strings (propertypaths). 
     * Elements/tokens are separated onto two stacks, operatorStack and operandStack
     * 
     * @param propertyPath
     */
    private void fillStacks(String propertyPath) {
        if (!propertyPath.startsWith(String.valueOf(END_OF_PATH_DELIMITER))) {
            operatorStack.push(String.valueOf(END_OF_PATH_DELIMITER));
        }
        char nextDelimiter = getNextDelimiter(propertyPath);

        String term = propertyPath.substring(0, propertyPath.indexOf(nextDelimiter));
        while (!"".equals(propertyPath) && !"".equals(term)) {
            if (!(END_OF_PATH_DELIMITER == nextDelimiter)) {
                term = propertyPath.substring(0, propertyPath.indexOf(nextDelimiter));
            } else {
                term = propertyPath;

            }
            if (term != null && !("".equals(term))) {
                operandStack.push(term);
            }
            if (!(END_OF_PATH_DELIMITER == nextDelimiter)) {
                operatorStack.push(String.valueOf(nextDelimiter));
            }
            if (!(END_OF_PATH_DELIMITER == nextDelimiter)) {
                propertyPath = propertyPath.substring(propertyPath.indexOf(nextDelimiter) + 1);
            } else {
                propertyPath = "";
            }
            nextDelimiter = getNextDelimiter(propertyPath);
        }
        if (LOG.isDebugEnabled()) {
            LOG.debug("Parsing finished, stacks filled, looking like this:");
            LOG.debug("OperandStack:\t" + operandStack.toString());
            LOG.debug("OperatorStack:\t" + operatorStack.toString());
        }

    }

    private void clearStacks() {
        operandStack.clear();
        operand = null;
        operatorStack.clear();
        operator = null;
    }

    /**
     * used for parsing a propertypath, returns next delimiter in a given string
     * @param propertyPath
     * @return
     */
    private char getNextDelimiter(String propertyPath) {
        for (char c : propertyPath.toCharArray()) {
            switch (c) {
            case LINK_TYPE_DELIMITER:
                return LINK_TYPE_DELIMITER;
            case CHILD_TYPE_DELIMITER:
                return CHILD_TYPE_DELIMITER;
            case PARENT_TYPE_DELIMITER:
                return PARENT_TYPE_DELIMITER;
            default:
                continue;
            }
        }
        return END_OF_PATH_DELIMITER;
    }

    /**
     * @return the results (the dataSet data)
     */
    public List<List<String>> getResults() {
        List<List<String>> tmpList = new LinkedList<List<String>>();

        List<String> keyList = new LinkedList<String>(resultMap.keySet());
        Collections.sort(keyList);

        List<String> keyListCleaned = cleanUpKeyList(keyList);

        for (String cleanKey : keyListCleaned) {
            List<String> list = resultMap.get(cleanKey).getPropertyList();
            Collections.replaceAll(list, null, "");
            tmpList.add(list);
        }
        this.table = new ArrayList<List<String>>();
        table.addAll(tmpList);

        Collections.sort(table, new Comparator<List<String>>() {

            @Override
            public int compare(List<String> row1, List<String> row2) {
                return compareRows(row1, row2);
            }

        });

        if (LOG.isDebugEnabled()) {
            LOG.debug("Result looks like:\t" + table.toString());
        }
        return table;
    }

    private int compareRows(List<String> row1, List<String> row2) {
        return compareRows(row1, row2, 0);
    }

    private int compareRows(List<String> row1, List<String> row2, int i) {
        int result = 0;
        if (row1.size() > i && row2.size() > i && row1.get(i) != null && row2.get(i) != null) {
            result = NSC.compare(row1.get(i), row2.get(i));
            if (result == 0) {
                result = compareRows(row1, row2, i + 1);
            }
        }
        return result;
    }

    private List<String> cleanUpKeyList(List<String> keyList) {
        List<String> keyListCleaned = new LinkedList<String>();
        String lastKey = null;
        for (String key : keyList) {
            if (lastKey != null) {
                if (!key.startsWith(lastKey) || key.equals(lastKey)) {
                    keyListCleaned.add(lastKey);
                }
            }
            lastKey = key;
        }
        if (!keyListCleaned.contains(lastKey)) {
            keyListCleaned.add(lastKey);
        }
        return keyListCleaned;
    }

    private void logElement(String rowId, String newRowId) {
        if (LOG.isInfoEnabled()) {
            LOG.info(
                    "Handling " + element.getTitle() + " (" + element.getTypeId() + "," + element.getDbId() + ") ");
            LOG.info("Row id: " + rowId + ", new row id: " + newRowId);
            LOG.info("Operand: " + operand + ", operand stack: " + operandStack.toString());
            LOG.info("Operator: " + operator + ", operator stack: " + operatorStack.toString() + "\n\n");
        }
    }
}