org.apache.blur.manager.writer.MutatableAction.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.blur.manager.writer.MutatableAction.java

Source

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF 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.apache.blur.manager.writer;

import static org.apache.blur.metrics.MetricsConstants.BLUR;
import static org.apache.blur.metrics.MetricsConstants.ORG_APACHE_BLUR;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;

import org.apache.blur.analysis.FieldManager;
import org.apache.blur.lucene.search.IndexSearcherCloseable;
import org.apache.blur.server.ShardContext;
import org.apache.blur.server.TableContext;
import org.apache.blur.thrift.BException;
import org.apache.blur.thrift.MutationHelper;
import org.apache.blur.thrift.generated.BlurException;
import org.apache.blur.thrift.generated.Column;
import org.apache.blur.thrift.generated.FetchRecordResult;
import org.apache.blur.thrift.generated.Record;
import org.apache.blur.thrift.generated.RecordMutation;
import org.apache.blur.thrift.generated.RecordMutationType;
import org.apache.blur.thrift.generated.Row;
import org.apache.blur.thrift.generated.RowMutation;
import org.apache.blur.thrift.generated.RowMutationType;
import org.apache.blur.utils.BlurConstants;
import org.apache.blur.utils.RowDocumentUtil;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.document.StringField;
import org.apache.lucene.index.AtomicReader;
import org.apache.lucene.index.AtomicReaderContext;
import org.apache.lucene.index.DocsEnum;
import org.apache.lucene.index.Fields;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.Terms;
import org.apache.lucene.index.TermsEnum;
import org.apache.lucene.search.DocIdSetIterator;
import org.apache.lucene.util.BytesRef;

import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Meter;
import com.yammer.metrics.core.MetricName;

public class MutatableAction extends IndexAction {

    private static final Meter _writeRecordsMeter;
    private static final Meter _writeRowMeter;

    static {
        MetricName metricName1 = new MetricName(ORG_APACHE_BLUR, BLUR, "Write Records/s");
        MetricName metricName2 = new MetricName(ORG_APACHE_BLUR, BLUR, "Write Row/s");
        _writeRecordsMeter = Metrics.newMeter(metricName1, "Records/s", TimeUnit.SECONDS);
        _writeRowMeter = Metrics.newMeter(metricName2, "Row/s", TimeUnit.SECONDS);
    }

    static abstract class BaseRecordMutatorIterator implements Iterable<Record> {

        private final Iterable<Record> _iterable;
        private final Map<String, Record> _records;

        public BaseRecordMutatorIterator(Iterable<Record> iterable, List<Record> records) {
            _iterable = iterable;
            _records = new TreeMap<String, Record>();
            for (Record r : records) {
                _records.put(r.getRecordId(), r);
            }
        }

        protected abstract Record handleRecordMutate(Record existingRecord, Record newRecord);

        @Override
        public Iterator<Record> iterator() {
            final Iterator<Record> iterator = _iterable.iterator();
            return new Iterator<Record>() {

                private SortedSet<String> _needToBeApplied = new TreeSet<String>(_records.keySet());
                private boolean _append = false;

                @Override
                public boolean hasNext() {
                    boolean hasNext = iterator.hasNext();
                    if (hasNext) {
                        return true;
                    }
                    if (areAllApplied()) {
                        return false; // Already applied changes, finished.
                    }
                    _append = true;
                    return true; // Still need to add new records.
                }

                private boolean areAllApplied() {
                    return _needToBeApplied.size() == 0;
                }

                @Override
                public Record next() {
                    if (_append) {
                        String first = _needToBeApplied.first();
                        _needToBeApplied.remove(first);
                        return _records.get(first);
                    }
                    Record record = iterator.next();
                    String recordId = record.getRecordId();
                    Record newRecord = _records.get(recordId);
                    if (newRecord != null) {
                        record = handleRecordMutate(record, newRecord);
                        _needToBeApplied.remove(recordId);
                    }
                    return record;
                }

                @Override
                public void remove() {
                    throw new RuntimeException("Not Supported.");
                }

            };
        }
    }

    static class UpdateRow extends InternalAction {

        static abstract class UpdateRowAction {
            abstract IterableRow performAction(IterableRow row);
        }

        private final List<UpdateRowAction> _actions = new ArrayList<UpdateRowAction>();

        private UpdateRowAction _deleteRecordsAction;
        private final Set<String> _deleteRecordsActionRecordsIdToDelete = new HashSet<String>();
        private UpdateRowAction _appendColumnsAction;
        private List<Record> _appendColumnsActionRecords = new ArrayList<Record>();
        private UpdateRowAction _replaceColumnsAction;
        private List<Record> _replaceColumnsActionRecords = new ArrayList<Record>();
        private UpdateRowAction _replaceRecordAction;
        private List<Record> _replaceRecordActionRecords = new ArrayList<Record>();

        private final String _rowId;
        private final TableContext _tableContext;
        private final FieldManager _fieldManager;

        UpdateRow(String rowId, TableContext tableContext) {
            _rowId = rowId;
            _tableContext = tableContext;
            _fieldManager = _tableContext.getFieldManager();
        }

        void deleteRecord(final String recordId) {
            if (_deleteRecordsAction == null) {
                _deleteRecordsAction = new UpdateRowAction() {
                    @Override
                    IterableRow performAction(IterableRow row) {
                        if (row == null) {
                            return null;
                        } else {
                            return new IterableRow(row.getRowId(),
                                    new DeleteRecordIterator(row, _deleteRecordsActionRecordsIdToDelete));
                        }
                    }
                };
                _actions.add(_deleteRecordsAction);
            }
            _deleteRecordsActionRecordsIdToDelete.add(recordId);
        }

        static class DeleteRecordIterator implements Iterable<Record> {

            private final Set<String> _recordsIdToDelete;
            private final Iterable<Record> _iterable;

            public DeleteRecordIterator(Iterable<Record> iterable, Set<String> recordsIdToDelete) {
                _recordsIdToDelete = recordsIdToDelete;
                _iterable = iterable;
            }

            @Override
            public Iterator<Record> iterator() {
                final GenericPeekableIterator<Record> iterator = GenericPeekableIterator.wrap(_iterable.iterator());
                return new Iterator<Record>() {

                    @Override
                    public boolean hasNext() {
                        Record record = iterator.peek();
                        if (record == null) {
                            return false;
                        }
                        if (_recordsIdToDelete.contains(record.getRecordId())) {
                            iterator.next();// Eat the delete
                            return hasNext();// Move to the next record
                        }
                        return iterator.hasNext();
                    }

                    @Override
                    public Record next() {
                        return iterator.next();
                    }

                    @Override
                    public void remove() {
                        throw new RuntimeException("Not Supported.");
                    }
                };
            }

        }

        void appendColumns(final Record record) {
            if (_appendColumnsAction == null) {
                _appendColumnsAction = new UpdateRowAction() {
                    @Override
                    IterableRow performAction(IterableRow row) {
                        if (row == null) {
                            return new IterableRow(_rowId, _appendColumnsActionRecords);
                        } else {
                            return new IterableRow(row.getRowId(),
                                    new AppendColumnsIterator(row, _appendColumnsActionRecords));
                        }
                    }
                };
                _actions.add(_appendColumnsAction);
            }
            _appendColumnsActionRecords.add(record);
        }

        static class AppendColumnsIterator extends BaseRecordMutatorIterator {

            public AppendColumnsIterator(Iterable<Record> iterable, List<Record> records) {
                super(iterable, records);
            }

            @Override
            protected Record handleRecordMutate(Record existingRecord, Record newRecord) {
                for (Column column : newRecord.getColumns()) {
                    existingRecord.addToColumns(column);
                }
                return existingRecord;
            }

        }

        void replaceColumns(final Record record) {
            if (_replaceColumnsAction == null) {
                _replaceColumnsAction = new UpdateRowAction() {
                    @Override
                    IterableRow performAction(IterableRow row) {
                        if (row == null) {
                            return new IterableRow(_rowId, _replaceColumnsActionRecords);
                        } else {
                            return new IterableRow(row.getRowId(),
                                    new ReplaceColumnsIterator(row, _replaceColumnsActionRecords));
                        }
                    }
                };
                _actions.add(_replaceColumnsAction);
            }
            _replaceColumnsActionRecords.add(record);
        }

        static class ReplaceColumnsIterator extends BaseRecordMutatorIterator {

            public ReplaceColumnsIterator(Iterable<Record> iterable, List<Record> records) {
                super(iterable, records);
            }

            @Override
            protected Record handleRecordMutate(Record existingRecord, Record newRecord) {
                return replaceColumns(existingRecord, newRecord);
            }

        }

        protected static Record replaceColumns(Record existing, Record newRecord) {
            Map<String, List<Column>> existingColumns = getColumnMap(existing.getColumns());
            Map<String, List<Column>> newColumns = getColumnMap(newRecord.getColumns());
            existingColumns.putAll(newColumns);
            Record record = new Record();
            record.setFamily(existing.getFamily());
            record.setRecordId(existing.getRecordId());
            record.setColumns(toList(existingColumns.values()));
            return record;
        }

        private static List<Column> toList(Collection<List<Column>> values) {
            ArrayList<Column> list = new ArrayList<Column>();
            for (List<Column> v : values) {
                list.addAll(v);
            }
            return list;
        }

        private static Map<String, List<Column>> getColumnMap(List<Column> columns) {
            Map<String, List<Column>> columnMap = new TreeMap<String, List<Column>>();
            for (Column column : columns) {
                String name = column.getName();
                List<Column> list = columnMap.get(name);
                if (list == null) {
                    list = new ArrayList<Column>();
                    columnMap.put(name, list);
                }
                list.add(column);
            }
            return columnMap;
        }

        void replaceRecord(final Record record) {
            if (_replaceRecordAction == null) {
                _replaceRecordAction = new UpdateRowAction() {
                    @Override
                    IterableRow performAction(IterableRow row) {
                        if (row == null) {
                            // New Row
                            return new IterableRow(_rowId, _replaceRecordActionRecords);
                        } else {
                            // Existing Row
                            return new IterableRow(row.getRowId(),
                                    new ReplaceRecordIterator(row, _replaceRecordActionRecords));
                        }
                    }
                };
                _actions.add(_replaceRecordAction);
            }
            _replaceRecordActionRecords.add(record);
        }

        static class ReplaceRecordIterator extends BaseRecordMutatorIterator {

            public ReplaceRecordIterator(Iterable<Record> iterable, List<Record> records) {
                super(iterable, records);
            }

            @Override
            protected Record handleRecordMutate(Record existingRecord, Record newRecord) {
                return newRecord;
            }

        }

        @Override
        void performAction(IndexSearcherCloseable searcher, IndexWriter writer) throws IOException {
            IterableRow iterableRow = getIterableRow(_rowId, searcher);
            for (UpdateRowAction action : _actions) {
                iterableRow = action.performAction(iterableRow);
            }
            Term term = createRowId(_rowId);
            if (iterableRow != null) {
                RecordToDocumentIterable docsToUpdate = new RecordToDocumentIterable(iterableRow, _fieldManager);
                Iterator<Iterable<Field>> iterator = docsToUpdate.iterator();
                final GenericPeekableIterator<Iterable<Field>> gpi = GenericPeekableIterator.wrap(iterator);
                if (gpi.peek() != null) {
                    writer.updateDocuments(term, wrapPrimeDoc(new Iterable<Iterable<Field>>() {
                        @Override
                        public Iterator<Iterable<Field>> iterator() {
                            return gpi;
                        }
                    }));
                } else {
                    writer.deleteDocuments(term);
                }
                _writeRecordsMeter.mark(docsToUpdate.count());
            }
            _writeRowMeter.mark();
        }

        private static class AtomicReaderTermsEnum {
            AtomicReader _atomicReader;
            TermsEnum _termsEnum;

            AtomicReaderTermsEnum(AtomicReader atomicReader, TermsEnum termsEnum) {
                _atomicReader = atomicReader;
                _termsEnum = termsEnum;
            }
        }

        private IterableRow getIterableRow(String rowId, IndexSearcherCloseable searcher) throws IOException {
            IndexReader indexReader = searcher.getIndexReader();
            BytesRef rowIdRef = new BytesRef(rowId);
            List<AtomicReaderTermsEnum> possibleRowIds = new ArrayList<AtomicReaderTermsEnum>();
            for (AtomicReaderContext atomicReaderContext : indexReader.leaves()) {
                AtomicReader atomicReader = atomicReaderContext.reader();
                Fields fields = atomicReader.fields();
                if (fields == null) {
                    continue;
                }
                Terms terms = fields.terms(BlurConstants.ROW_ID);
                if (terms == null) {
                    continue;
                }
                TermsEnum termsEnum = terms.iterator(null);
                if (!termsEnum.seekExact(rowIdRef, true)) {
                    continue;
                }
                // need atomic read as well...
                possibleRowIds.add(new AtomicReaderTermsEnum(atomicReader, termsEnum));
            }
            if (possibleRowIds.isEmpty()) {
                return null;
            }
            return new IterableRow(rowId, getRecords(possibleRowIds));
        }

        private Iterable<Record> getRecords(final List<AtomicReaderTermsEnum> possibleRowIds) {
            return new Iterable<Record>() {
                @Override
                public Iterator<Record> iterator() {
                    final List<DocsEnum> docsEnums = new ArrayList<DocsEnum>();
                    for (AtomicReaderTermsEnum atomicReaderTermsEnum : possibleRowIds) {
                        try {
                            docsEnums.add(atomicReaderTermsEnum._termsEnum
                                    .docs(atomicReaderTermsEnum._atomicReader.getLiveDocs(), null));
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    return new Iterator<Record>() {

                        private int _index = 0;
                        private boolean _nextCalled;
                        private int _docId;

                        @Override
                        public boolean hasNext() {
                            try {
                                if (_nextCalled) {
                                    if (_docId == DocIdSetIterator.NO_MORE_DOCS) {
                                        return false;
                                    }
                                    return true;
                                }
                                while (true) {
                                    if (_index >= docsEnums.size()) {
                                        _nextCalled = true;
                                        _docId = DocIdSetIterator.NO_MORE_DOCS;
                                        return false;
                                    }
                                    DocsEnum docsEnum = docsEnums.get(_index);
                                    int docId = docsEnum.nextDoc();
                                    if (docId != DocIdSetIterator.NO_MORE_DOCS) {
                                        _nextCalled = true;
                                        _docId = docId;
                                        return true;
                                    }
                                    _index++;
                                }
                            } catch (IOException e) {
                                throw new RuntimeException(e);
                            }
                        }

                        @Override
                        public Record next() {
                            _nextCalled = false;
                            AtomicReaderTermsEnum atomicReaderTermsEnum = possibleRowIds.get(_index);
                            try {
                                Document document = atomicReaderTermsEnum._atomicReader.document(_docId);
                                FetchRecordResult fetchRecordResult = RowDocumentUtil.getRecord(document);
                                return fetchRecordResult.getRecord();
                            } catch (IOException e) {
                                throw new RuntimeException(e);
                            }
                        }

                        @Override
                        public void remove() {
                            throw new RuntimeException("Not Supported.");
                        }
                    };
                }
            };
        }

        Iterable<Iterable<Field>> wrapPrimeDoc(final Iterable<Iterable<Field>> iterable) {
            return new Iterable<Iterable<Field>>() {

                @Override
                public Iterator<Iterable<Field>> iterator() {
                    final Iterator<Iterable<Field>> iterator = iterable.iterator();
                    return new Iterator<Iterable<Field>>() {

                        private boolean _first = true;

                        @Override
                        public boolean hasNext() {
                            return iterator.hasNext();
                        }

                        @Override
                        public Iterable<Field> next() {
                            Iterable<Field> fields = iterator.next();
                            if (_first) {
                                _first = false;
                                return addPrimeDocField(fields);
                            } else {
                                return fields;
                            }
                        }

                        private Iterable<Field> addPrimeDocField(Iterable<Field> fields) {
                            return new IterablePlusOne<Field>(new StringField(BlurConstants.PRIME_DOC,
                                    BlurConstants.PRIME_DOC_VALUE, Store.NO), fields);
                        }

                        @Override
                        public void remove() {
                            throw new RuntimeException("Not Supported.");
                        }

                    };
                }
            };
        }

    }

    static abstract class InternalAction {
        abstract void performAction(IndexSearcherCloseable searcher, IndexWriter writer) throws IOException;
    }

    private final List<InternalAction> _actions = new ArrayList<InternalAction>();
    private final Map<String, UpdateRow> _rowUpdates = new HashMap<String, UpdateRow>();
    private final FieldManager _fieldManager;
    private final TableContext _tableContext;

    public MutatableAction(ShardContext context) {
        _tableContext = context.getTableContext();
        _fieldManager = _tableContext.getFieldManager();
    }

    public void deleteRow(final String rowId) {
        _actions.add(new InternalAction() {
            @Override
            void performAction(IndexSearcherCloseable searcher, IndexWriter writer) throws IOException {
                writer.deleteDocuments(createRowId(rowId));
                _writeRowMeter.mark();
            }
        });
    }

    public void replaceRow(final Row row) {
        _actions.add(new InternalAction() {
            @Override
            void performAction(IndexSearcherCloseable searcher, IndexWriter writer) throws IOException {
                List<List<Field>> docs = RowDocumentUtil.getDocs(row, _fieldManager);
                Term rowId = createRowId(row.getId());
                writer.updateDocuments(rowId, docs);
                _writeRecordsMeter.mark(docs.size());
                _writeRowMeter.mark();
            }
        });
    }

    public void deleteRecord(final String rowId, final String recordId) {
        UpdateRow updateRow = getUpdateRow(rowId);
        updateRow.deleteRecord(recordId);
    }

    public void replaceRecord(final String rowId, final Record record) {
        UpdateRow updateRow = getUpdateRow(rowId);
        updateRow.replaceRecord(record);
    }

    public void appendColumns(final String rowId, final Record record) {
        UpdateRow updateRow = getUpdateRow(rowId);
        updateRow.appendColumns(record);
    }

    public void replaceColumns(final String rowId, final Record record) {
        UpdateRow updateRow = getUpdateRow(rowId);
        updateRow.replaceColumns(record);
    }

    @Override
    public void performMutate(IndexSearcherCloseable searcher, IndexWriter writer) throws IOException {
        try {
            for (InternalAction internalAction : _actions) {
                internalAction.performAction(searcher, writer);
            }
        } finally {
            _actions.clear();
        }
    }

    public static Term createRowId(String id) {
        return new Term(BlurConstants.ROW_ID, id);
    }

    public static Term createRecordId(String id) {
        return new Term(BlurConstants.RECORD_ID, id);
    }

    private synchronized UpdateRow getUpdateRow(String rowId) {
        UpdateRow updateRow = _rowUpdates.get(rowId);
        if (updateRow == null) {
            updateRow = new UpdateRow(rowId, _tableContext);
            _rowUpdates.put(rowId, updateRow);
            _actions.add(updateRow);
        }
        return updateRow;
    }

    @Override
    public void doPreCommit(IndexSearcherCloseable indexSearcher, IndexWriter writer) {

    }

    @Override
    public void doPostCommit(IndexWriter writer) {

    }

    @Override
    public void doPreRollback(IndexWriter writer) {

    }

    @Override
    public void doPostRollback(IndexWriter writer) {

    }

    public void mutate(RowMutation mutation) {
        RowMutationType type = mutation.rowMutationType;
        switch (type) {
        case REPLACE_ROW:
            Row row = MutationHelper.getRowFromMutations(mutation.rowId, mutation.recordMutations);
            replaceRow(row);
            break;
        case UPDATE_ROW:
            doUpdateRowMutation(mutation, this);
            break;
        case DELETE_ROW:
            deleteRow(mutation.rowId);
            break;
        default:
            throw new RuntimeException("Not supported [" + type + "]");
        }
    }

    private void doUpdateRowMutation(RowMutation mutation, MutatableAction mutatableAction) {
        String rowId = mutation.getRowId();
        for (RecordMutation recordMutation : mutation.getRecordMutations()) {
            RecordMutationType type = recordMutation.recordMutationType;
            Record record = recordMutation.getRecord();
            switch (type) {
            case DELETE_ENTIRE_RECORD:
                mutatableAction.deleteRecord(rowId, record.getRecordId());
                break;
            case APPEND_COLUMN_VALUES:
                mutatableAction.appendColumns(rowId, record);
                break;
            case REPLACE_ENTIRE_RECORD:
                mutatableAction.replaceRecord(rowId, record);
                break;
            case REPLACE_COLUMNS:
                mutatableAction.replaceColumns(rowId, record);
                break;
            default:
                throw new RuntimeException("Unsupported record mutation type [" + type + "]");
            }
        }
    }

    public void mutate(List<RowMutation> mutations) {
        for (int i = 0; i < mutations.size(); i++) {
            mutate(mutations.get(i));
        }
    }

    public static List<RowMutation> reduceMutates(List<RowMutation> mutations) throws BlurException {
        Map<String, RowMutation> mutateMap = new TreeMap<String, RowMutation>();
        for (RowMutation mutation : mutations) {
            if (mutation.getRowId() == null) {
                throw new BException("Mutation has null rowid [{0}]", mutation);
            }
            RowMutation rowMutation = mutateMap.get(mutation.getRowId());
            if (rowMutation != null) {
                mutateMap.put(mutation.getRowId(), merge(rowMutation, mutation));
            } else {
                mutateMap.put(mutation.getRowId(), mutation);
            }
        }
        return new ArrayList<RowMutation>(mutateMap.values());
    }

    private static RowMutation merge(RowMutation mutation1, RowMutation mutation2) throws BlurException {
        RowMutationType rowMutationType1 = mutation1.getRowMutationType();
        RowMutationType rowMutationType2 = mutation2.getRowMutationType();
        if (!rowMutationType1.equals(rowMutationType2)) {
            throw new BException(
                    "RowMutation conflict, cannot perform 2 different operations on the same row in the same batch. [{0}] [{1}]",
                    mutation1, mutation2);
        }
        if (rowMutationType1.equals(RowMutationType.DELETE_ROW)) {
            // Since both are trying to delete the same row, just pick one and move
            // on.
            return mutation1;
        } else if (rowMutationType1.equals(RowMutationType.REPLACE_ROW)) {
            throw new BException(
                    "RowMutation conflict, cannot perform 2 different REPLACE_ROW mutations on the same row in the same batch. [{0}] [{1}]",
                    mutation1, mutation2);
        } else {
            // Now this is a row update, so try to merge the record mutations
            List<RecordMutation> recordMutations1 = mutation1.getRecordMutations();
            List<RecordMutation> recordMutations2 = mutation2.getRecordMutations();
            List<RecordMutation> mergedRecordMutations = merge(recordMutations1, recordMutations2);
            mutation1.setRecordMutations(mergedRecordMutations);
            return mutation1;
        }
    }

    private static List<RecordMutation> merge(List<RecordMutation> recordMutations1,
            List<RecordMutation> recordMutations2) throws BException {
        Map<String, RecordMutation> recordMutationMap = new TreeMap<String, RecordMutation>();
        merge(recordMutations1, recordMutationMap);
        merge(recordMutations2, recordMutationMap);
        return new ArrayList<RecordMutation>(recordMutationMap.values());
    }

    private static void merge(List<RecordMutation> recordMutations, Map<String, RecordMutation> recordMutationMap)
            throws BException {
        for (RecordMutation recordMutation : recordMutations) {
            Record record = recordMutation.getRecord();
            String recordId = record.getRecordId();
            RecordMutation existing = recordMutationMap.get(recordId);
            if (existing != null) {
                recordMutationMap.put(recordId, merge(recordMutation, existing));
            } else {
                recordMutationMap.put(recordId, recordMutation);
            }
        }
    }

    private static RecordMutation merge(RecordMutation recordMutation1, RecordMutation recordMutation2)
            throws BException {
        RecordMutationType recordMutationType1 = recordMutation1.getRecordMutationType();
        RecordMutationType recordMutationType2 = recordMutation2.getRecordMutationType();
        if (!recordMutationType1.equals(recordMutationType2)) {
            throw new BException(
                    "RecordMutation conflict, cannot perform 2 different operations on the same record in the same row in the same batch. [{0}] [{1}]",
                    recordMutation1, recordMutation2);
        }

        if (recordMutationType1.equals(RecordMutationType.DELETE_ENTIRE_RECORD)) {
            // Since both are trying to delete the same record, just pick one and move
            // on.
            return recordMutation1;
        } else if (recordMutationType1.equals(RecordMutationType.REPLACE_ENTIRE_RECORD)) {
            throw new BException(
                    "RecordMutation conflict, cannot perform 2 different replace record operations on the same record in the same row in the same batch. [{0}] [{1}]",
                    recordMutation1, recordMutation2);
        } else if (recordMutationType1.equals(RecordMutationType.REPLACE_COLUMNS)) {
            throw new BException(
                    "RecordMutation conflict, cannot perform 2 different replace columns operations on the same record in the same row in the same batch. [{0}] [{1}]",
                    recordMutation1, recordMutation2);
        } else {
            Record record1 = recordMutation1.getRecord();
            Record record2 = recordMutation2.getRecord();
            String family1 = record1.getFamily();
            String family2 = record2.getFamily();

            if (isSameFamily(family1, family2)) {
                record1.getColumns().addAll(record2.getColumns());
                return recordMutation1;
            } else {
                throw new BException(
                        "RecordMutation conflict, cannot merge records with different family. [{0}] [{1}]",
                        recordMutation1, recordMutation2);
            }
        }
    }

    private static boolean isSameFamily(String family1, String family2) {
        if (family1 == null && family2 == null) {
            return true;
        }
        if (family1 != null && family1.equals(family2)) {
            return true;
        }
        return false;
    }
}