GeoKnow/Jassa-Core

View on GitHub
lib/sponate/Engine.js

Summary

Maintainability
C
1 day
Test Coverage
var Class = require('../ext/Class');

var ListServiceUtils = require('./ListServiceUtils');
var AccUtils = require('./AccUtils');

var HashMap = require('../util/collection/HashMap');
var HashSet = require('../util/collection/HashSet');

var LookupServiceUtils = require('./LookupServiceUtils');
var ListServiceUtils = require('./ListServiceUtils');

var ListServiceTransformItems = require('../service/list_service/ListServiceTransformItems');

var ObjectUtils = require('../util/ObjectUtils');

var Slot = require('../util/Slot');


// var _ = require('lodash');
var forEach = require('lodash.foreach');

var shared = require('../util/shared');
var Promise = shared.Promise;
var PromiseUtils = require('../util/PromiseUtils');


var Engine = {
    indexAccMap : function(state, sourceName, nodeToAcc) {
        var map = state[sourceName];
        if (!map) {
            map = new HashMap();
            state[sourceName] = map;
        }

        map.putMap(nodeToAcc);
    },

    mergeRefs : function(open, refs) {
        refs.forEach(function(ref) {
            var refSpec = ref.getRefSpec();
            var sourceName = refSpec.getTarget();
            var refValue = ref.getRefValue();

            var set;
            if (!(sourceName in open)) {
                set = new HashSet();
                open[sourceName] = set;
            } else {
                set = open[sourceName];
            }

            set.add(refValue);
        });
    },

    // Return a new array of refs that are not already covered by the state
    // i.e. only return those refs which still need to be resolved
    filterRefs : function(state, refs) {
        var result = [];

        refs.forEach(function(ref) {
            var refSpec = ref.getRefSpec();
            var sourceName = refSpec.getTarget();
            var refValue = ref.getRefValue();

            var map = state[sourceName];

            var isResolved = map && map.containsKey(refValue);
            if (!isResolved) {
                result.push(ref);
            }
        });

        return result;
    },

    buildObjs : function(state) {
        var result = {};

        // Retrieve the value of each acc and extend the original object with it
        // At this point we do not resolve references yet
        forEach(state, function(srcMap, sourceName) {

            var objMap = new HashMap();
            result[sourceName] = objMap;

            srcMap.entries().forEach(function(entry) {
                var id = entry.key;
                var acc = entry.val;

                var val = acc.getValue();
                objMap.put(id, val);
            });
        });

        return result;
    },

    /**
     * For each object retrieve its lazy slots and reverse the order, so that
     * we can evaluate the lazy functions in a depth first manner (i.e. parent lazy transform
     * will only be executed after the child transforms have been applied)
     */

    // FIXME This is bugged, because the system does not descend into the lazy slots objects
    // So nested transformations break
    buildLazySlots: function(sourceToIdtoObj, attr, result, descend) {
        result = result || [];

        forEach(sourceToIdtoObj, function(srcMap, sourceName) {
            srcMap.values().forEach(function(obj) {
                var tmp = Engine.buildSlots(obj, attr, null, descend);
                tmp = tmp.reverse();
                result.push.apply(result, tmp);
            });
        });

        return result;
    },

    // descend: whether to descend into slots once they were found
    buildSlots: function(obj, attr, result, descend) {
        result = result || [];

        if (Array.isArray(obj)) {

            obj.forEach(function(item, index) {
                var isSlot = item && item[attr];
                if(isSlot) {
                    var slot = new Slot(obj, index, item[attr]);
                    result.push(slot);
                }

                if(!isSlot || descend) {
                    Engine.buildSlots(item, attr, result, descend);
                }
            });

        } else if (ObjectUtils.isObject(obj)) {

            forEach(obj, function(v, k) {
                var isSlot = v && v[attr];
                if (isSlot) {
                    var slot = new Slot(obj, k, v[attr]);
                    result.push(slot);
                }

                if(!isSlot || descend) {
                    Engine.buildSlots(v, attr, result, descend);
                }
            });

        } /*
             * else { // Nothing to do }
             */

        return result;
    },

    createLookupService : function(decls, sourceName) {
        var source = decls.getSource(sourceName);
        if (!source) {
            throw new Error('No source mapping with name ' + sourceName
                    + ' found');
        }

        var sparqlService = source.getSparqlService();
        var mappedConcept = source.getMappedConcept();
        var result = LookupServiceUtils.createLookupServiceMappedConceptAcc(
                sparqlService, mappedConcept);
        return result;
    },

    createListService : function(decls, sourceName, isLeftJoin) {

        // var sourceName = query.getSourceName();

        var source = decls.getSource(sourceName);
        if (!source) {
            throw new Error('No source mapping with name ' + sourceName
                    + ' found');
        }

        var sparqlService = source.getSparqlService();
        var mappedConcept = source.getMappedConcept();

        if(mappedConcept.getConcept().getVar().getName() === 'rowId') {
            throw new Error('rowId cannot be used as the root ID of a MappedConcept');
        }

        var listServiceAcc = ListServiceUtils.createListServiceAcc(
                sparqlService, mappedConcept, isLeftJoin);

        var self = this;

        var result = new ListServiceTransformItems(listServiceAcc, function(accEntries) {
            //var collectedState = self.collectState(decls, sourceName, accEntries);

            var r =
                self.collectState(decls, sourceName, accEntries)
                .spread(function(rootIds, state, p) {
                    var s = self.postProcess(state, sourceName, rootIds);
                    return s;
                });

            return r;
        });

        return result;
    },

    collectState : function(decls, sourceName, accEntries) {
        // Do the initial concept based lookup
        var state = {};

        // Get the initial ids
        var rootIds = accEntries.map(function(accEntry) { // TODO We could use
                                                            // _.pluck here
            return accEntry.key;
        });

        // Collect the accs
        var map = new HashMap();
        var open = {};

        accEntries.forEach(function(accEntry) {
            var acc = accEntry.val;

            // Note: We expect instances of AccMap here!
            var state = acc.getState();

            map.putMap(state);

            var refs = AccUtils.getRefs(acc);

            Engine.mergeRefs(open, refs);
        });

        // console.log('OPEN: ' + JSON.stringify(open, null, 4));

        state[sourceName] = map;
        var result = this.resolveRefs(decls, open, state).then(function(p) {
            var r = [ rootIds, state, p ];
            return r;
        });

        return result;
    },

    postProcess : function(state, sourceName, rootIds) {

        // Retain all references
        var accRefs = [];
        forEach(state, function(srcMap) {
            var accs = srcMap.values();
            var refs = AccUtils.getRefs(accs);

            accRefs.push.apply(accRefs, refs);
        });

        //console.log('AccRefs: ', accRefs);

        accRefs.forEach(function(accRef) {
            var refSpec = accRef.getRefSpec();
            var targetName = refSpec.getTarget();
            var refValue = accRef.getRefValue();

            accRef.setBaseValue({
                _ref : {
                    targetName : targetName,
                    refValue : refValue,
                    attr : refSpec.getAttr()
                }
            });
        });

        var sourceToIdToObj = Engine.buildObjs(state);

        //try {
        var refSlots = Engine.buildSlots(sourceToIdToObj, '_ref');

        var lazySlots = Engine.buildLazySlots(sourceToIdToObj, '_lazy', null, true);

//        } catch(err) {
//            console.log('err: ', err);
//        }
        //console.log('Got ' + slots.length + ' slots');

        refSlots.forEach(function(slot) {
            var meta = slot.getMeta();

            var idToObj = sourceToIdToObj[meta.targetName];
            var obj = idToObj.get(meta.refValue);

            var attr = meta.attr;
            obj = (obj != null && attr != null) ? obj[attr] : obj;

            //console.log('SLOT: ' + meta + ' ' + meta.attr + ' ' + obj);

            slot.setValue(obj);
        });


        lazySlots.forEach(function(slot) {
            var meta = slot.getMeta();

            var fn = meta.fn;
            var v = meta.value;

            var replacement = fn(v);
            slot.setValue(replacement);
        });

        // Apply lazy functions
       //var slots = Engine.buildSlots(sourceToIdToObj, '_lazy');

        // Prepare the result
        var result = rootIds.map(function(rootId) {
            var idToObj = sourceToIdToObj[sourceName];
            var obj = idToObj.get(rootId);
            var r = {
                key : rootId,
                val : obj
            };
            return r;
        });

        return result;
    },

    exec: function(decls, query) {
        var sourceName = query.getSourceName();
        var listService = this.createListService(decls, sourceName, query
                .isLeftJoin());

        var limit = query.getLimit();
        var offset = query.getOffset();
        var filterConcept = query.getFilterConcept();

        var result = listService.fetchItems(filterConcept, limit, offset);

        return result;
    },

    /**
     * open is a Map<SourceName, Set<ObjectId>> state is a Map<SourceName,
     * Map<ObjectId, Document>>
     */
    resolveRefs: function(decls, open, state) {

        var self = this;
        var sourceNames = Object.keys(open);

        // console.log('SOURCE NAMES: ', sourceNames);

        var subPromises = sourceNames.map(function(sourceName) {

            // console.log('XXXNAMES: ' + sourceName);

            var set = open[sourceName];

            var lookupService = self.createLookupService(decls, sourceName);
            var nodes = set.entries();

            var subPromise = lookupService.lookup(nodes).then(function(nodeToAcc) {
                var accs = nodeToAcc.values();
                // console.log('accs: ' + JSON.stringify(accs));
                // if(true) { throw new Error('foo'); }

                Engine.indexAccMap(state, sourceName, nodeToAcc);
                var refs = AccUtils.getRefs(accs);
                var openRefs = Engine.filterRefs(state, refs);
                var next = {};
                Engine.mergeRefs(next, openRefs);

                var r = self.resolveRefs(decls, next, state);
                return r;
            });

            return subPromise;
        });

        return PromiseUtils.all(subPromises);
    },

};

module.exports = Engine;