eu.esdihumboldt.hale.common.align.merge.impl.AbstractMergeCellMigrator.java Source code

Java tutorial

Introduction

Here is the source code for eu.esdihumboldt.hale.common.align.merge.impl.AbstractMergeCellMigrator.java

Source

/*
 * Copyright (c) 2017 wetransform GmbH
 * 
 * All rights reserved. This program and the accompanying materials are made
 * available 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.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution. If not, see <http://www.gnu.org/licenses/>.
 * 
 * Contributors:
 *     wetransform GmbH <http://www.wetransform.to>
 */

package eu.esdihumboldt.hale.common.align.merge.impl;

import static eu.esdihumboldt.hale.common.align.migrate.util.MigrationUtil.isDirectMatch;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Collectors;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps;

import eu.esdihumboldt.cst.functions.groovy.GroovyJoin;
import eu.esdihumboldt.hale.common.align.merge.MergeCellMigrator;
import eu.esdihumboldt.hale.common.align.merge.MergeIndex;
import eu.esdihumboldt.hale.common.align.merge.MergeSettings;
import eu.esdihumboldt.hale.common.align.merge.MergeUtil;
import eu.esdihumboldt.hale.common.align.migrate.AlignmentMigration;
import eu.esdihumboldt.hale.common.align.migrate.CellMigrator;
import eu.esdihumboldt.hale.common.align.migrate.MigrationOptions;
import eu.esdihumboldt.hale.common.align.migrate.impl.DefaultCellMigrator;
import eu.esdihumboldt.hale.common.align.migrate.impl.MigrationOptionsImpl;
import eu.esdihumboldt.hale.common.align.model.AlignmentUtil;
import eu.esdihumboldt.hale.common.align.model.Cell;
import eu.esdihumboldt.hale.common.align.model.CellUtil;
import eu.esdihumboldt.hale.common.align.model.Entity;
import eu.esdihumboldt.hale.common.align.model.EntityDefinition;
import eu.esdihumboldt.hale.common.align.model.MutableCell;
import eu.esdihumboldt.hale.common.align.model.ParameterValue;
import eu.esdihumboldt.hale.common.align.model.annotations.messages.CellLog;
import eu.esdihumboldt.hale.common.align.model.functions.JoinFunction;
import eu.esdihumboldt.hale.common.align.model.functions.join.JoinParameter;
import eu.esdihumboldt.hale.common.align.model.functions.join.JoinParameter.JoinCondition;
import eu.esdihumboldt.hale.common.align.model.impl.DefaultCell;
import eu.esdihumboldt.hale.common.align.model.impl.PropertyEntityDefinition;
import eu.esdihumboldt.hale.common.align.model.impl.TypeEntityDefinition;
import eu.esdihumboldt.hale.common.core.io.Value;
import eu.esdihumboldt.hale.common.core.report.SimpleLog;
import eu.esdihumboldt.hale.common.instance.model.Filter;
import eu.esdihumboldt.hale.common.schema.model.PropertyDefinition;
import eu.esdihumboldt.hale.common.schema.model.constraint.type.GeometryType;

/**
 * Cell merger base class.
 * 
 * @author Simon Templer
 * @param <C> the context class
 */
public abstract class AbstractMergeCellMigrator<C> extends DefaultCellMigrator implements MergeCellMigrator {

    @Override
    public Iterable<MutableCell> mergeCell(Cell originalCell, MergeIndex mergeIndex, AlignmentMigration migration,
            Function<String, CellMigrator> getCellMigrator, SimpleLog log) {
        MutableCell copy = new DefaultCell(originalCell);

        // update source entities
        if (copy.getSource() != null && !copy.getSource().isEmpty()) {
            ListMultimap<String, ? extends Entity> sources = copy.getSource();
            return mergeSources(sources, mergeIndex, originalCell, migration, getCellMigrator, log);
        } else {
            // no sources - return original
            return Collections.singleton(copy);
        }
    }

    /**
     * Create a new context object.
     * 
     * @param originalCell the original cell
     * 
     * @return the new context object
     */
    protected abstract C newContext(Cell originalCell);

    /**
     * Update the cell sources.
     * 
     * @param sources the old sources
     * @param mergeIndex the merge index
     * @param originalCell the original cell
     * @param migration the alignment migration (may be useful for cases where
     *            only entity replacement needs to be done)
     * @param getCellMigrator functions that yields a cell migrator for a
     *            function (may be useful for cases where only entity
     *            replacement needs to be done)
     * @param log the migration process log
     * @return the merged cell or cells
     */
    protected Iterable<MutableCell> mergeSources(ListMultimap<String, ? extends Entity> sources,
            MergeIndex mergeIndex, Cell originalCell, AlignmentMigration migration,
            Function<String, CellMigrator> getCellMigrator, SimpleLog log) {
        boolean transferBase = true; // XXX relevant here at all?

        if (sources.size() == 1) {
            EntityDefinition source = sources.values().iterator().next().getDefinition();
            List<Cell> matches = mergeIndex.getCellsForTarget(source);

            List<String> storedMessages = new ArrayList<>();
            boolean inaccurateMatch = false;
            if (matches.isEmpty()) {
                // try to find match via parent (in specific cases)
                matches = findParentMatch(source, mergeIndex);
                if (!matches.isEmpty()) {
                    inaccurateMatch = true;
                    // message may not be added in every case, because it may be
                    // a duplicate
                    //               storedMessages.add(MessageFormat
                    //                     .format("Inaccurate match of {0} via parent entity", source));
                }
            }

            if (!matches.isEmpty()) {
                List<MutableCell> cells = new ArrayList<>();
                for (Cell match : matches) {
                    // create a result cell for each match

                    // if the matching is a Retype/Rename, replace source of
                    // the original cell
                    if (isDirectMatch(match)) {
                        MigrationOptions replaceSource = new MigrationOptionsImpl(true, false, transferBase);
                        cells.add(getCellMigrator.apply(originalCell.getTransformationIdentifier())
                                .updateCell(originalCell, migration, replaceSource, log));
                    }
                    // if the cell is a Retype/Rename, replace the target of
                    // matching cell
                    else if (isDirectMatch(originalCell)) {
                        MigrationOptions replaceTarget = new MigrationOptionsImpl(false, true, transferBase);
                        AlignmentMigration cellMigration = new AbstractMigration() {

                            @Override
                            protected Optional<EntityDefinition> findMatch(EntityDefinition entity) {
                                Entity target = CellUtil.getFirstEntity(originalCell.getTarget());
                                if (target != null) {
                                    return Optional.ofNullable(target.getDefinition());
                                }
                                return Optional.empty();
                            }
                        };
                        MutableCell newCell = getCellMigrator.apply(match.getTransformationIdentifier())
                                .updateCell(match, cellMigration, replaceTarget, log);
                        SimpleLog cellLog = SimpleLog.all(log, new CellLog(newCell, CELL_LOG_CATEGORY));

                        // source of original cell may have
                        // filters/conditions/contexts that are not applied by
                        // changing the target

                        // try to apply source contexts
                        Entity originalSource = CellUtil.getFirstEntity(originalCell.getSource());
                        applySourceContexts(newCell, originalSource, migration, cellLog);

                        cells.add(newCell);
                    } else {
                        // otherwise, use custom logic to try to combine cells

                        MutableCell newCell = new DefaultCell(originalCell);
                        SimpleLog cellLog = SimpleLog.all(log, new CellLog(newCell, CELL_LOG_CATEGORY));
                        C context = newContext(originalCell);

                        // reset source
                        newCell.setSource(ArrayListMultimap.create());

                        if (inaccurateMatch) {
                            cellLog.warn(MessageFormat.format("Inaccurate match of {0} via parent entity", source));
                        }

                        mergeSource(newCell, sources.keys().iterator().next(), source, match, originalCell, cellLog,
                                context, migration, mergeIndex);

                        finalize(newCell, migration, context, cellLog);

                        cells.add(newCell);
                    }
                }

                if (!cells.isEmpty() && !storedMessages.isEmpty()) {
                    // add stored messages
                    cells.forEach(cell -> {
                        CellLog cLog = new CellLog(cell, CELL_LOG_CATEGORY);
                        storedMessages.forEach(msg -> cLog.warn(msg));
                    });
                }

                return cells;
            } else {
                // no match -> remove?
                // rather add original + documentation
                MutableCell newCell = new DefaultCell(originalCell);
                SimpleLog cellLog = SimpleLog.all(log, new CellLog(newCell, CELL_LOG_CATEGORY));

                cellLog.warn("No match for source {0} found, unable to associate to new source schema", source);

                return Collections.singleton(newCell);
            }
        } else {
            // handle each source

            // collects messages in case all matches are direct matches
            List<String> directMessages = new ArrayList<>();

            // determine if all matches are direct
            boolean allDirect = sources.entries().stream().allMatch(source -> {
                List<Cell> matches = mergeIndex.getCellsForTarget(source.getValue().getDefinition());
                if (matches.isEmpty()) {
                    directMessages.add(MessageFormat.format(
                            "No match was found for source {0}, please check how this can be compensated.",
                            source.getValue().getDefinition()));
                    // if there is no match, treat it as direct match
                    return true;
                } else {
                    if (matches.size() > 1) {
                        directMessages.add(MessageFormat.format(
                                "Multiple matches for source {0}, only one was taken into account",
                                source.getValue().getDefinition()));
                    }

                    return isDirectMatch(matches.get(0));
                }
            });

            MutableCell newCell;

            if (allDirect) {
                // if the matching are all Retype/Rename, replace sources of
                // the original cell
                MigrationOptions replaceSource = new MigrationOptionsImpl(true, false, transferBase);
                newCell = getCellMigrator.apply(originalCell.getTransformationIdentifier()).updateCell(originalCell,
                        migration, replaceSource, log);
                // add messages from match check
                SimpleLog cellLog = SimpleLog.all(log, new CellLog(newCell, CELL_LOG_CATEGORY));
                directMessages.forEach(msg -> cellLog.warn(msg));
            } else {
                // handle each source separately
                newCell = new DefaultCell(originalCell);
                SimpleLog cellLog = SimpleLog.all(log, new CellLog(newCell, CELL_LOG_CATEGORY));
                C context = newContext(originalCell);

                // reset source
                newCell.setSource(ArrayListMultimap.create());

                for (Entry<String, ? extends Entity> source : sources.entries()) {
                    List<Cell> matches = mergeIndex.getCellsForTarget(source.getValue().getDefinition());
                    if (!matches.isEmpty()) {
                        Cell match = matches.get(0);

                        mergeSource(newCell, source.getKey(), source.getValue().getDefinition(), match,
                                originalCell, cellLog, context, migration, mergeIndex);

                        if (matches.size() > 1) {
                            // FIXME how can we deal w/ multiple matches?
                            cellLog.warn("Multiple matches for source {0}, only one was handled",
                                    source.getValue().getDefinition());
                        }
                    } else {
                        // no match, just not add source?
                        cellLog.warn("No match was found for source {0}, please check how this can be compensated.",
                                source.getValue().getDefinition());
                    }
                }

                finalize(newCell, migration, context, cellLog);
            }

            return Collections.singleton(newCell);
        }
    }

    private List<Cell> findParentMatch(EntityDefinition entity, MergeIndex mergeIndex) {
        // XXX only allow parent matches for specific cases right now
        if (!(entity.getDefinition() instanceof PropertyDefinition)
                || !((PropertyDefinition) entity.getDefinition()).getPropertyType()
                        .getConstraint(GeometryType.class).isGeometry()) {
            // not a geometry
            return Collections.emptyList();
        }

        while (entity != null) {
            entity = AlignmentUtil.getParent(entity);

            List<Cell> matches = mergeIndex.getCellsForTarget(entity);
            if (!matches.isEmpty()) {
                return matches;
            }
        }

        return Collections.emptyList();
    }

    /**
     * Apply contexts/conditions from the original source to the source of the
     * new mapping cell that replaces it.
     * 
     * @param newCell the cell to adapt
     * @param originalSource the original source
     * @param migration the alignment migration
     * @param log the cell log
     */
    protected void applySourceContexts(MutableCell newCell, Entity originalSource, AlignmentMigration migration,
            SimpleLog log) {
        if (originalSource != null) {
            EntityDefinition original = originalSource.getDefinition();
            boolean isDefault = AlignmentUtil.isDefaultEntity(original);

            if (!isDefault) {
                ListMultimap<String, ? extends Entity> newSource = newCell.getSource();
                if (newSource == null || newSource.size() == 0) {
                    // new cell does not have a source -> drop contexts
                    log.warn(
                            "Any conditions/contexts on the original source have been dropped because the new mapping does not have a source: "
                                    + MergeUtil.getContextInfoString(original));
                } else if (newSource.size() == 1) {
                    // try to transfer contexts
                    Entity singleSource = CellUtil.getFirstEntity(newSource);
                    if (singleSource != null) {
                        EntityDefinition transferedSource = AbstractMigration.translateContexts(original,
                                singleSource.getDefinition(), migration, log);
                        ListMultimap<String, Entity> s = ArrayListMultimap.create();
                        s.put(newSource.keySet().iterator().next(), AlignmentUtil.createEntity(transferedSource));
                        newCell.setSource(s);
                    }
                } else {
                    // no idea where to add contexts
                    // TODO assess where filter/contexts can be best applied?
                    // -> alternative to translateContexts that handles all
                    // sources?

                    // XXX for now only special case handling to support
                    // XtraServer use case
                    if (applySourceContextsToJoinFocus(newCell, originalSource, migration, log)) {
                        return;
                    }

                    // no idea where to add contexts -> report
                    log.warn(
                            "Any conditions/contexts on the original source have been dropped because the new mapping has multiple sources and it is not clear where they should be attached: "
                                    + MergeUtil.getContextInfoString(original));
                }
            }
        }
    }

    /**
     * Handle special case of applying source contexts to the entity that is the
     * Join focus.
     * 
     * @param newCell the new cell to update the sources
     * @param originalSource the original source
     * @param migration the alignment migration
     * @param log the operation log
     * @return if the method handled the context transfer
     */
    private boolean applySourceContextsToJoinFocus(MutableCell newCell, Entity originalSource,
            AlignmentMigration migration, SimpleLog log) {
        if (!MergeSettings.isTransferContextsToJoinFocus()) {
            return false;
        }

        String function = newCell.getTransformationIdentifier();
        switch (function) {
        case GroovyJoin.ID:
        case JoinFunction.ID:
            break;
        default:
            return false;
        }

        JoinParameter joinConfig = CellUtil.getFirstParameter(newCell, JoinFunction.PARAMETER_JOIN)
                .as(JoinParameter.class);

        if (joinConfig == null || joinConfig.getTypes() == null || joinConfig.getTypes().isEmpty()) {
            return false;
        }

        TypeEntityDefinition focus = joinConfig.getTypes().iterator().next();
        AtomicReference<Filter> focusFilter = new AtomicReference<>();

        // transfer context
        newCell.setSource(ArrayListMultimap.create(Multimaps.transformValues(newCell.getSource(),
                new com.google.common.base.Function<Entity, Entity>() {

                    @Override
                    public Entity apply(Entity input) {
                        if (input.getDefinition().getPropertyPath().isEmpty()
                                && input.getDefinition().getType().equals(focus.getType())) {
                            EntityDefinition transferedSource = AbstractMigration.translateContexts(
                                    originalSource.getDefinition(), input.getDefinition(), migration, log);
                            focusFilter.set(transferedSource.getFilter());
                            return AlignmentUtil.createEntity(transferedSource);
                        } else {
                            return input;
                        }
                    }
                })));

        // fix filter in order and conditions
        // XXX only works like this because a type currently can only be present
        // once in the source

        if (focusFilter.get() != null) {
            // order
            List<TypeEntityDefinition> types = new ArrayList<>();
            for (int i = 0; i < joinConfig.getTypes().size(); i++) {
                TypeEntityDefinition type = joinConfig.getTypes().get(i);
                if (i == 0) {
                    type = new TypeEntityDefinition(type.getDefinition(), type.getSchemaSpace(), focusFilter.get());
                }
                types.add(type);
            }

            // conditions
            Set<JoinCondition> conditions = joinConfig.getConditions().stream().map(c -> {
                if (c.baseProperty.getType().equals(focus.getType())) {
                    return new JoinCondition(applyFilter(c.baseProperty, focusFilter.get()), c.joinProperty);
                } else {
                    return c;
                }
            }).collect(Collectors.toSet());

            JoinParameter newConfig = new JoinParameter(types, conditions);

            ListMultimap<String, ParameterValue> modParams = ArrayListMultimap
                    .create(newCell.getTransformationParameters());
            List<ParameterValue> joinParams = modParams.get(JoinFunction.PARAMETER_JOIN);
            if (!joinParams.isEmpty()) {
                JoinParameter joinParam = joinParams.get(0).as(JoinParameter.class);
                if (joinParam != null) {
                    joinParams.clear();
                    joinParams.add(new ParameterValue(Value.complex(newConfig)));
                }
            }
            newCell.setTransformationParameters(modParams);
        }

        return true;
    }

    private PropertyEntityDefinition applyFilter(PropertyEntityDefinition property, Filter filter) {
        if (filter != null) {
            return new PropertyEntityDefinition(property.getType(), property.getPropertyPath(),
                    property.getSchemaSpace(), filter);
        } else {
            return property;
        }
    }

    /**
     * Finalize a created cell based on the context.
     * 
     * @param newCell the new merged cell
     * @param migration the alignment migration
     * @param context the cell merge context
     * @param log the cell log
     */
    protected void finalize(MutableCell newCell, AlignmentMigration migration, C context, SimpleLog log) {
        // override me
    }

    /**
     * Merge a source according to a matching cell.
     * 
     * @param cell the target cell to merge the source to (contains only already
     *            merged sources)
     * @param sourceName the name of the source
     * @param source the source definition
     * @param match the match for the source
     * @param originalCell the original cell
     * @param log the migration process log
     * @param context the context for the creation of the cell (possibly shared
     *            across multiple calls to
     *            {@link #mergeSource(MutableCell, String, EntityDefinition, Cell, Cell, SimpleLog, Object, AlignmentMigration, MergeIndex)}
     *            )
     * @param migration the alignment migration
     * @param mergeIndex the merge index
     */
    protected void mergeSource(MutableCell cell, String sourceName, EntityDefinition source, Cell match,
            Cell originalCell, SimpleLog log, C context, AlignmentMigration migration, MergeIndex mergeIndex) {
        // override me
        log.warn("Unsupported combination {0} / {1} for source {2}", match.getTransformationIdentifier(),
                cell.getTransformationIdentifier(), source);
    }

}