org.jumpmind.symmetric.io.data.writer.TransformWriter.java Source code

Java tutorial

Introduction

Here is the source code for org.jumpmind.symmetric.io.data.writer.TransformWriter.java

Source

/**
 * Licensed to JumpMind Inc under one or more contributor
 * license agreements.  See the NOTICE file distributed
 * with this work for additional information regarding
 * copyright ownership.  JumpMind Inc licenses this file
 * to you under the GNU General Public License, version 3.0 (GPLv3)
 * (the "License"); you may not use this file except in compliance
 * with the License.
 *
 * You should have received a copy of the GNU General Public License,
 * version 3.0 (GPLv3) along with this library; if not, see
 * <http://www.gnu.org/licenses/>.
 *
 * 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.jumpmind.symmetric.io.data.writer;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.jumpmind.db.model.Table;
import org.jumpmind.db.platform.IDatabasePlatform;
import org.jumpmind.symmetric.io.data.Batch;
import org.jumpmind.symmetric.io.data.CsvData;
import org.jumpmind.symmetric.io.data.DataContext;
import org.jumpmind.symmetric.io.data.DataEventType;
import org.jumpmind.symmetric.io.data.IDataWriter;
import org.jumpmind.symmetric.io.data.transform.DeleteAction;
import org.jumpmind.symmetric.io.data.transform.IColumnTransform;
import org.jumpmind.symmetric.io.data.transform.IgnoreColumnException;
import org.jumpmind.symmetric.io.data.transform.IgnoreRowException;
import org.jumpmind.symmetric.io.data.transform.NewAndOldValue;
import org.jumpmind.symmetric.io.data.transform.TransformColumn;
import org.jumpmind.symmetric.io.data.transform.TransformColumn.IncludeOnType;
import org.jumpmind.symmetric.io.data.transform.TransformColumnException;
import org.jumpmind.symmetric.io.data.transform.TransformPoint;
import org.jumpmind.symmetric.io.data.transform.TransformTable;
import org.jumpmind.symmetric.io.data.transform.TransformedData;
import org.jumpmind.util.Statistics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TransformWriter extends NestedDataWriter {

    protected static final Logger log = LoggerFactory.getLogger(TransformWriter.class);

    protected TransformPoint transformPoint;
    protected IDatabasePlatform platform;
    protected Map<String, List<TransformTable>> transformsBySourceTable;
    protected Table sourceTable;
    protected List<TransformTable> activeTransforms;
    protected Batch batch;
    protected Map<String, IColumnTransform<?>> columnTransforms;
    protected Table lastTransformedTable;

    public TransformWriter(IDatabasePlatform platform, TransformPoint transformPoint, IDataWriter targetWriter,
            Map<String, IColumnTransform<?>> columnTransforms, TransformTable... transforms) {
        super(targetWriter);
        this.columnTransforms = columnTransforms;
        this.platform = platform;
        this.transformPoint = transformPoint == null ? TransformPoint.LOAD : transformPoint;
        this.transformsBySourceTable = toMap(transforms);
    }

    protected Map<String, List<TransformTable>> toMap(TransformTable[] transforms) {
        Map<String, List<TransformTable>> transformsByTable = new HashMap<String, List<TransformTable>>();
        if (transforms != null) {
            for (TransformTable transformTable : transforms) {
                if (transformPoint == transformTable.getTransformPoint()) {
                    String sourceTableName = transformTable.getFullyQualifiedSourceTableName().toLowerCase();
                    List<TransformTable> tables = transformsByTable.get(sourceTableName);
                    if (tables == null) {
                        tables = new ArrayList<TransformTable>();
                        transformsByTable.put(sourceTableName, tables);
                    }
                    tables.add(transformTable);
                }
            }
        }
        return transformsByTable;
    }

    @Override
    public void start(Batch batch) {
        this.batch = batch;
        super.start(batch);
    }

    @Override
    public boolean start(Table table) {
        activeTransforms = transformsBySourceTable.get(table.getFullyQualifiedTableName().toLowerCase());
        if (activeTransforms != null && activeTransforms.size() > 0) {
            this.sourceTable = table;
            return true;
        } else {
            this.sourceTable = null;
            return super.start(table);
        }
    }

    protected boolean isTransformable(DataEventType eventType) {
        return eventType != null && (eventType == DataEventType.INSERT || eventType == DataEventType.UPDATE
                || eventType == DataEventType.DELETE);
    }

    public void write(CsvData data) {
        DataEventType eventType = data.getDataEventType();
        if (activeTransforms != null && activeTransforms.size() > 0 && isTransformable(eventType)) {
            if (data.requiresTable() && sourceTable == null && context.getLastParsedTable() != null) {
                // if we cross batches and the table isn't specified, then
                // use the last table we used
                start(context.getLastParsedTable());
            }

            long ts = System.currentTimeMillis();
            Map<String, String> sourceValues = data.toColumnNameValuePairs(this.sourceTable.getColumnNames(),
                    CsvData.ROW_DATA);

            Map<String, String> oldSourceValues = null;
            if (data.contains(CsvData.OLD_DATA)) {
                oldSourceValues = data.toColumnNameValuePairs(this.sourceTable.getColumnNames(), CsvData.OLD_DATA);
            }

            Map<String, String> sourceKeyValues = null;
            if (data.contains(CsvData.PK_DATA)) {
                sourceKeyValues = data.toKeyColumnValuePairs(this.sourceTable);
            }

            if (eventType == DataEventType.DELETE) {
                sourceValues = oldSourceValues;

                if (sourceValues == null || sourceValues.size() == 0) {
                    sourceValues = sourceKeyValues;
                }
            }

            if (log.isDebugEnabled()) {
                log.debug("{} transformation(s) started because of {} on {}.  The original row data was: {}",
                        new Object[] { activeTransforms.size(), eventType.toString(),
                                this.sourceTable.getFullyQualifiedTableName(), sourceValues });
            }

            List<TransformedData> dataThatHasBeenTransformed = new ArrayList<TransformedData>();
            TransformTable[] transformTables = activeTransforms
                    .toArray(new TransformTable[activeTransforms.size()]);
            if (eventType == DataEventType.DELETE) {
                CollectionUtils.reverseArray(transformTables);
            }

            for (TransformTable transformation : transformTables) {
                transformation = transformation.enhanceWithImpliedColumns(
                        this.sourceTable.getPrimaryKeyColumnNames(), this.sourceTable.getColumnNames());
                if (eventType == DataEventType.INSERT && transformation.isUpdateFirst()) {
                    eventType = DataEventType.UPDATE;
                }
                dataThatHasBeenTransformed.addAll(transform(eventType, context, transformation, sourceKeyValues,
                        oldSourceValues, sourceValues));
            }

            for (TransformedData transformedData : dataThatHasBeenTransformed) {
                Table transformedTable = transformedData.buildTargetTable();
                CsvData csvData = transformedData.buildTargetCsvData();
                long transformTimeInMs = System.currentTimeMillis() - ts;
                boolean processData = true;
                if (lastTransformedTable == null || !lastTransformedTable.equals(transformedTable)) {
                    if (lastTransformedTable != null) {
                        this.nestedWriter.end(lastTransformedTable);
                    }
                    processData = this.nestedWriter.start(transformedTable);
                    if (!processData) {
                        lastTransformedTable = null;
                    } else {
                        lastTransformedTable = transformedTable;
                    }
                }
                if (processData || !csvData.requiresTable()) {
                    this.nestedWriter.write(csvData);
                }
                Statistics stats = this.nestedWriter.getStatistics().get(batch);
                if (stats != null) {
                    stats.increment(DataWriterStatisticConstants.TRANSFORMMILLIS, transformTimeInMs);
                }
                ts = System.currentTimeMillis();
            }

        } else {
            if (sourceTable != null) {
                super.start(sourceTable);
            }
            super.write(data);
            if (sourceTable != null) {
                super.end(sourceTable);
            }
        }

    }

    protected List<TransformedData> transform(DataEventType eventType, DataContext context,
            TransformTable transformation, Map<String, String> sourceKeyValues, Map<String, String> oldSourceValues,
            Map<String, String> sourceValues) {
        try {
            List<TransformedData> dataToTransform = create(context, eventType, transformation, sourceKeyValues,
                    oldSourceValues, sourceValues);
            List<TransformedData> dataThatHasBeenTransformed = new ArrayList<TransformedData>(
                    dataToTransform.size());
            if (log.isDebugEnabled()) {
                log.debug("{} target data was created for the {} transformation.  The target table is {}",
                        new Object[] { dataToTransform.size(), transformation.getTransformId(),
                                transformation.getFullyQualifiedTargetTableName() });
            }
            int transformNumber = 0;
            for (TransformedData targetData : dataToTransform) {
                transformNumber++;
                if (perform(context, targetData, transformation, sourceValues, oldSourceValues)) {
                    if (log.isDebugEnabled()) {
                        log.debug(
                                "Data has been transformed to a {} for the #{} transform.  The mapped target columns are: {}. The mapped target values are: {}",
                                new Object[] { targetData.getTargetDmlType().toString(), transformNumber,
                                        ArrayUtils.toString(targetData.getColumnNames()),
                                        ArrayUtils.toString(targetData.getColumnValues()) });
                    }
                    dataThatHasBeenTransformed.add(targetData);
                } else {
                    log.debug("Data has not been transformed for the #{} transform", transformNumber);
                }
            }
            return dataThatHasBeenTransformed;
        } catch (IgnoreRowException ex) {
            // ignore this row
            if (log.isDebugEnabled()) {
                log.debug("Transform indicated that the target row should be ignored with a target key of: {}",
                        "unknown.  Transformation aborted during tranformation of key");
            }
            return new ArrayList<TransformedData>(0);
        }

    }

    protected boolean perform(DataContext context, TransformedData data, TransformTable transformation,
            Map<String, String> sourceValues, Map<String, String> oldSourceValues) throws IgnoreRowException {
        boolean persistData = false;
        try {
            DataEventType eventType = data.getSourceDmlType();
            for (TransformColumn transformColumn : transformation.getTransformColumns()) {
                if (!transformColumn.isPk()) {
                    IncludeOnType includeOn = transformColumn.getIncludeOn();
                    if (includeOn == IncludeOnType.ALL
                            || (includeOn == IncludeOnType.INSERT && eventType == DataEventType.INSERT)
                            || (includeOn == IncludeOnType.UPDATE && eventType == DataEventType.UPDATE)
                            || (includeOn == IncludeOnType.DELETE && eventType == DataEventType.DELETE)) {
                        if (StringUtils.isBlank(transformColumn.getSourceColumnName())
                                || sourceValues.containsKey(transformColumn.getSourceColumnName())) {
                            try {
                                Object value = transformColumn(context, data, transformColumn, sourceValues,
                                        oldSourceValues);
                                if (value instanceof NewAndOldValue) {
                                    data.put(transformColumn, ((NewAndOldValue) value).getNewValue(),
                                            oldSourceValues != null ? ((NewAndOldValue) value).getOldValue() : null,
                                            false);
                                } else if (value == null || value instanceof String) {
                                    data.put(transformColumn, (String) value, null, false);
                                } else if (value instanceof List) {
                                    throw new IllegalStateException(String.format(
                                            "Column transform failed %s.%s. Transforms that multiply rows must be marked as part of the primary key",
                                            transformColumn.getTransformId(),
                                            transformColumn.getTargetColumnName()));
                                } else {
                                    throw new IllegalStateException(String.format(
                                            "Column transform failed %s.%s. It returned an unexpected type of %s",
                                            transformColumn.getTransformId(), transformColumn.getTargetColumnName(),
                                            value.getClass().getSimpleName()));
                                }
                            } catch (IgnoreColumnException e) {
                                // Do nothing. We are ignoring the column
                                if (log.isDebugEnabled()) {
                                    log.debug("A transform indicated we should ignore the target column {}",
                                            transformColumn.getTargetColumnName());
                                }
                            }
                        } else {
                            if (eventType != DataEventType.DELETE) {
                                log.warn("Could not find a source column of {} for the transformation: {}",
                                        transformColumn.getSourceColumnName(), transformation.getTransformId());
                            } else {
                                log.debug(
                                        "Could not find a source column of {} for the transformation: {}.  This is probably because this was a DELETE event and no old data was captured.",
                                        transformColumn.getSourceColumnName(), transformation.getTransformId());
                            }
                        }
                    }
                }
            }

            // perform a transformation if there are columns defined for
            // transformation
            if (data.getColumnNames().length > 0) {
                if (data.getTargetDmlType() != DataEventType.DELETE) {
                    persistData = true;
                } else {
                    // handle the delete action
                    DeleteAction deleteAction = transformation.getDeleteAction();
                    switch (deleteAction) {
                    case DEL_ROW:
                        data.setTargetDmlType(DataEventType.DELETE);
                        persistData = true;
                        break;
                    case UPDATE_COL:
                        data.setTargetDmlType(DataEventType.UPDATE);
                        persistData = true;
                        break;
                    case NONE:
                    default:
                        if (log.isDebugEnabled()) {
                            log.debug(
                                    "The {} transformation is not configured to delete row.  Not sending the delete through.",
                                    transformation.getTransformId());
                        }
                    }
                }
            }
        } catch (IgnoreRowException ex) {
            // ignore this row
            if (log.isDebugEnabled()) {
                log.debug("Transform indicated that the target row should be ignored with a target key of: {}",
                        ArrayUtils.toString(data.getKeyValues()));
            }
        }
        return persistData;
    }

    protected List<TransformedData> create(DataContext context, DataEventType dataEventType,
            TransformTable transformation, Map<String, String> sourceKeyValues, Map<String, String> oldSourceValues,
            Map<String, String> sourceValues) throws IgnoreRowException {
        List<TransformColumn> columns = transformation.getPrimaryKeyColumns();
        if (columns == null || columns.size() == 0) {
            log.error("No primary key defined for the transformation: {}", transformation.getTransformId());
            return new ArrayList<TransformedData>(0);
        } else {
            List<TransformedData> datas = new ArrayList<TransformedData>();
            TransformedData data = new TransformedData(transformation, dataEventType, sourceKeyValues,
                    oldSourceValues, sourceValues);
            datas.add(data);
            DataEventType eventType = data.getSourceDmlType();
            for (TransformColumn transformColumn : columns) {
                IncludeOnType includeOn = transformColumn.getIncludeOn();
                if (includeOn == IncludeOnType.ALL
                        || (includeOn == IncludeOnType.INSERT && eventType == DataEventType.INSERT)
                        || (includeOn == IncludeOnType.UPDATE && eventType == DataEventType.UPDATE)
                        || (includeOn == IncludeOnType.DELETE && eventType == DataEventType.DELETE)) {
                    List<TransformedData> newDatas = null;
                    try {
                        Object columnValue = transformColumn(context, data, transformColumn, sourceValues,
                                oldSourceValues);
                        if (columnValue instanceof List) {
                            @SuppressWarnings("unchecked")
                            List<String> values = (List<String>) columnValue;
                            if (values.size() > 0) {
                                data.put(transformColumn, values.get(0),
                                        oldSourceValues != null ? values.get(0) : null, true);
                                if (values.size() > 1) {
                                    if (newDatas == null) {
                                        newDatas = new ArrayList<TransformedData>(values.size() - 1);
                                    }
                                    for (int i = 1; i < values.size(); i++) {
                                        TransformedData newData = data.copy();
                                        newData.put(transformColumn, values.get(i),
                                                oldSourceValues != null ? values.get(i) : null, true);
                                        newDatas.add(newData);
                                    }
                                }
                            } else {
                                throw new IgnoreRowException();
                            }
                        } else if (columnValue instanceof NewAndOldValue) {
                            data.put(transformColumn, ((NewAndOldValue) columnValue).getNewValue(),
                                    oldSourceValues != null ? ((NewAndOldValue) columnValue).getOldValue() : null,
                                    true);
                        } else {
                            data.put(transformColumn, (String) columnValue,
                                    oldSourceValues != null ? (String) columnValue : null, true);
                        }
                    } catch (IgnoreColumnException e) {
                        // Do nothing. We are suppose to ignore the column.
                    }

                    if (newDatas != null) {
                        datas.addAll(newDatas);
                        newDatas = null;
                    }
                }
            }

            return datas;
        }
    }

    protected Object transformColumn(DataContext context, TransformedData data, TransformColumn transformColumn,
            Map<String, String> sourceValues, Map<String, String> oldSourceValues)
            throws IgnoreRowException, IgnoreColumnException {
        Object returnValue = null;
        String value = transformColumn.getSourceColumnName() != null
                ? sourceValues.get(transformColumn.getSourceColumnName())
                : null;
        returnValue = value;
        IColumnTransform<?> transform = columnTransforms != null
                ? columnTransforms.get(transformColumn.getTransformType())
                : null;
        if (transform != null) {
            try {
                String oldValue = null;
                if (oldSourceValues != null) {
                    oldValue = oldSourceValues.get(transformColumn.getSourceColumnName());
                }
                returnValue = transform.transform(platform, context, transformColumn, data, sourceValues, value,
                        oldValue);
            } catch (RuntimeException ex) {
                log.warn("Column transform failed {}.{} ({}) for source values of {}",
                        new Object[] { transformColumn.getTransformId(), transformColumn.getTargetColumnName(),
                                transformColumn.getIncludeOn().name(), sourceValues.toString() });
                throw ex;
            }
        } else {
            throw new TransformColumnException(String.format("Could not locate a column transform of type '%s'",
                    transformColumn.getTransformType()));
        }
        return returnValue;
    }

    public void end(Table table) {
        if (this.lastTransformedTable != null) {
            this.nestedWriter.end(lastTransformedTable);
            this.lastTransformedTable = null;
        }
        if (activeTransforms != null && activeTransforms.size() > 0) {
            activeTransforms = null;
        } else {
            super.end(table);
        }

    }

}