org.openepics.discs.conf.dl.common.AbstractDataLoader.java Source code

Java tutorial

Introduction

Here is the source code for org.openepics.discs.conf.dl.common.AbstractDataLoader.java

Source

/*
 * Copyright (c) 2014 European Spallation Source
 * Copyright (c) 2014 Cosylab d.d.
 *
 * This file is part of Controls Configuration Database.
 *
 * Controls Configuration Database is free software: you can redistribute it
 * and/or modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 2 of the License,
 * or any newer 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. If not, see https://www.gnu.org/licenses/gpl-2.0.txt
 */
package org.openepics.discs.conf.dl.common;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.annotation.Nullable;

import org.apache.commons.lang3.tuple.Pair;
import org.openepics.discs.conf.util.ImportFileStatistics;

import com.google.common.base.Preconditions;

/**
 * Skeleton for all data loaders.
 *
 * @author <a href="mailto:andraz.pozar@cosylab.com">Andra Poar</a>
 * @author <a href="mailto:miroslav.pavleski@cosylab.com">Miroslav Pavleski</a>
 * @author <a href="mailto:miha.vitorovic@cosylab.com">Miha Vitorovi?</a>
 */
public abstract class AbstractDataLoader implements DataLoader {
    /** The name of the first column in the Excel sheet */
    public static final String HDR_OPERATION = "OPERATION";

    /** Constant representing the index of the command (Operation) in the data row */
    protected static final int COL_INDEX_OPERATION = 0;

    /** The devices template starts with data in row 10 (0 based == 9) */
    public static final int DEFAULT_EXCEL_TAMPLATE_DATA_START_ROW = 9;

    /** The {@link DataLoaderResult} contains error information and state for the error loading process */
    protected DataLoaderResult result = new DataLoaderResult();

    /** Contextual data map */
    private Map<String, Object> contextualData;

    /** Indexes of all relevant fields within a row, stateful. */
    protected Map<String, Integer> indicies = null;

    /** For stateful row processing, represents the data for the current row, private, exposed to sub-classes by
     * {@link #readCurrentRowCellForHeader(String)}
     */
    private List<String> currentRowData = null;

    /** The command parsed for the current row. */
    private String command;

    @Override
    public int getImportDataStartIndex() {
        return DEFAULT_EXCEL_TAMPLATE_DATA_START_ROW;
    }

    @Override
    public abstract int getDataWidth();

    /**
     * Loads data from the input table of strings inputRows, into the database, gracefully handling errors.
     * Potential error messages and state are returned in the return value.
     *
     * @param inputRows a {@link List} of {@link Pair}s consisting of an integer representing excel row number
     * in left-hand position and a list of strings representing the cells for each column in that row
     * @param contextualData optional map of objects passed with string keys
     *
     * @return {@link DataLoaderResult} which represents error state &amp; information (or lack of)
     */
    @Override
    public DataLoaderResult loadDataToDatabase(List<Pair<Integer, List<String>>> inputRows,
            Map<String, Object> contextualData) {

        if (contextualData == null) {
            this.contextualData = new HashMap<>();
        } else {
            this.contextualData = contextualData;
        }

        init();

        int dataRows = 0;
        int createRows = 0;
        int updateRows = 0;
        int deleteRows = 0;

        currentRowData = null;
        for (Pair<Integer, List<String>> row : inputRows) {
            result.resetRowError();

            result.setCurrentRowNumber(row.getLeft());
            currentRowData = row.getRight();

            if (DataLoader.CMD_END.equals(currentRowData.get(COL_INDEX_OPERATION))) {
                break;
            } else {
                ++dataRows;

                command = checkCommandAndRequiredFields();
                if (command == null) {
                    continue;
                }

                assignMembersForCurrentRow();
                if (result.isRowError()) {
                    continue;
                }

                switch (command) {
                case DataLoader.CMD_UPDATE:
                case DataLoader.CMD_UPDATE_DEVICE:
                case DataLoader.CMD_UPDATE_PROPERTY:
                case DataLoader.CMD_UPDATE_DEVICE_TYPE:
                case DataLoader.CMD_UPDATE_ENTITY:
                    handleUpdate(command);
                    ++updateRows;
                    break;
                case DataLoader.CMD_DELETE:
                case DataLoader.CMD_DELETE_DEVICE:
                case DataLoader.CMD_DELETE_PROPERTY:
                case DataLoader.CMD_DELETE_DEVICE_TYPE:
                case DataLoader.CMD_DELETE_ENTITY:
                case DataLoader.CMD_DELETE_ENTITY_AND_CHILDREN:
                case DataLoader.CMD_DELETE_RELATION:
                case DataLoader.CMD_UNINSTALL:
                    handleDelete(command);
                    ++deleteRows;
                    break;
                case DataLoader.CMD_CREATE:
                case DataLoader.CMD_CREATE_DEVICE:
                case DataLoader.CMD_CREATE_DEVICE_TYPE:
                case DataLoader.CMD_CREATE_PROPERTY:
                case DataLoader.CMD_CREATE_ENTITY:
                case DataLoader.CMD_CREATE_RELATION:
                case DataLoader.CMD_INSTALL:
                    handleCreate(command);
                    ++createRows;
                    break;
                default:
                    result.addRowMessage(ErrorMessage.COMMAND_NOT_VALID, HDR_OPERATION, command);
                }
            }
        }
        result.setImportFileStatistics(new ImportFileStatistics(dataRows, createRows, updateRows, deleteRows));
        return result;
    }

    /**
     * <p>
     * An method invoked from {@link #loadDataToDatabase(List, Map)} prior data-loading
     * is initiated.
     * </p>
     * <p>
     * Should be used by sub-classes to initialize shared state. In sub-classes {@link #getFromContext(String)} can
     * be called to get context-specific objects passed in {@link #loadDataToDatabase(List, Map)}
     * </p>
     */
    protected void init() {
        result.clear();
        setUpIndexesForFields();
    }

    /**
     * Sub-classes should implement this abstract method to define the name of the unique column (a column used do
     * uniquely identify a data row, for example a column containing entity names or serial numbers)
     *
     * If the method returns null, there is no unique column in the spreadsheet.
     *
     * A unique column has special constraints around it (i.e. it must have a value)..
     *
     * @return the header column name of the unique column
     */
    protected abstract @Nullable Integer getUniqueColumnIndex();

    /**
     * Invoked by {@link #loadDataToDatabase(List, Map)} for sub-classes to initialize row-bound
     * state (class fields) from row-data.
     */
    protected abstract void assignMembersForCurrentRow();

    /**
     * <p>
     * Handle a row that contains an <code>UPDATE</code> command.
     * </p>
     * <p>
     * <b>Precondition:</b> {@link #assignMembersForCurrentRow()} has been invoked. This gives chance
     * to the sub-class to extract row data for the call to this method.
     * </p>
     * @param actualCommand the actual command used in update (update entity itself or its property)
     */
    protected abstract void handleUpdate(String actualCommand);

    /**
     * <p>
     * Handle a row that contains a <code>DELETE</code> command.
     * </p>
     * <p>
     * <b>Precondition:</b> {@link #assignMembersForCurrentRow()} has been invoked. This gives chance
     * to the sub-class to extract row data for the call to this method.
     * </p>
     * @param actualCommand the actual command used in delete (delete entity itself or its property)
     */
    protected abstract void handleDelete(String actualCommand);

    /**
     * <p>
     * Handle a row that contains a <code>CREATE</code> command.
     * </p>
     * <p>
     * <b>Precondition:</b> {@link #assignMembersForCurrentRow()} has been invoked. This gives chance
     * to the sub-class to extract row data for the call to this method.
     * </p>
     * @param actualCommand the actual command used in create (create entity itself or its property)
     */
    protected abstract void handleCreate(String actualCommand);

    /**
     * Sub-classes should use this to get data from the context passed from caller.
     *
     * @param key the key
     * @return contextual data
     */
    protected Object getFromContext(String key) {
        return contextualData.get(key);
    }

    /**
     * Checks the command field and availability of necessary data in the row cells
     *
     * @return the {@link String} command or <code>null</code> if checks failed
     */
    private String checkCommandAndRequiredFields() {
        Preconditions.checkNotNull(indicies);

        final String command = currentRowData.get(COL_INDEX_OPERATION);

        final @Nullable Integer uniqueIndex = getUniqueColumnIndex();

        for (Entry<String, Integer> indexEntry : indicies.entrySet()) {
            final String columnName = indexEntry.getKey();
            final Integer index = indexEntry.getValue();
            Preconditions.checkNotNull(index);

            // Check if data is missing for given conditions
            if ((uniqueIndex != null) && uniqueIndex.equals(index)) {
                if (currentRowData.get(index) == null) {
                    result.addRowMessage(ErrorMessage.REQUIRED_FIELD_MISSING, columnName);
                }
            }
        }
        if (result.isRowError()) {
            return null;
        }

        return command;
    }

    /**
     * Sets up a map of row data indices based on the template header row data. This method is implemented in the
     * actual data loader implementation.
     */
    protected abstract void setUpIndexesForFields();

    /**
     * During row processing, returns a {@link String} cell within the row that belongs to given column
     *
     * @param index The index of the column to read
     * @return the contents of the appropriate column cell within the current row
     */
    protected String readCurrentRowCellForHeader(int index) {
        return currentRowData.get(index);
    }

    /**
     * Handles all errors that happen if the user is not authorized to perform the operation
     *
     * @param logger    Instance of logger to log the exception to server log
     * @param e         Exception that was caught in data loader implementation
     */
    protected void handleLoadingError(Logger logger, Exception e) { // NOSONAR
        logger.log(Level.FINE, e.getMessage(), e);
        if (e.getCause() instanceof org.openepics.discs.conf.security.SecurityException) {
            result.addRowMessage(ErrorMessage.NOT_AUTHORIZED, HDR_OPERATION, command);
        } else {
            result.addRowMessage(ErrorMessage.SYSTEM_EXCEPTION);
        }
    }
}