/*
* Copyright (c) 2014 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.db.joiner;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* MapBuilder's job is to construct arbitrarily complex maps of query results.
* Maps can be nested like this: Map<A, Map<B, Set<C>>> which means A maps to a map
* of B to a set of C objects. To construct such a map, you would
* pushList("A").pushList("B").pushSet("C").map().
* So the pushes are read left to right just like the items in the nested maps.
*
* Two things are complicated about this code:
* 1. It needs to understand how to make a map Map<A,Set<C>> where A is joined to B and
* B is joined to C. That is, it needs to traverse intermediate joins not used in the maps.
* 2. It needs to understand how to traverse the joins in reverse, that is it needs to be
* able to make a map Map<B, Set<A>> where A is joined to B. That is what reverseDuples()
* is about.
*
* @author root
*
*/
class MapBuilder {
List<MapBuilderTerm> terms = new ArrayList<MapBuilderTerm>();
Joiner joiner;
Map previousResult = null;
/**
* Constructor.
*
* @param joiner
*/
MapBuilder(Joiner joiner) {
this.joiner = joiner;
}
/**
* Add a term to the map builder.
*
* @param type
* @param jclass
*/
void addTerm(MapBuilderTermType type, JClass jclass, String alias) {
MapBuilderTerm newTerm = new MapBuilderTerm();
newTerm.type = type;
newTerm.jclass = jclass;
newTerm.alias = alias;
terms.add(newTerm);
}
Map buildMapStructure() {
if (terms.size() < 2) {
throw new JoinerException("Map must consist of at least two terms");
}
// For the 2nd through nth terms, search backwards finding the join path to previous term
for (int i = 1; i < terms.size(); i++) {
List<JClass> joinPath = computeJoinPath(terms.get(i - 1), terms.get(i));
terms.get(i - 1).joinPath = joinPath;
}
// Now, starting at last two terms, compute the duples URIs representing what is joined.
// Then format the Map for this item.
for (int i = terms.size() - 2; i >= 0; i--) {
Map<URI, Set<URI>> duples = computeDuples(terms.get(i).joinPath);
if (!terms.get(i).alias.equals(terms.get(i).joinPath.get(0).getAlias())) {
// If working in reverse, reverse the duples.
duples = reverseDuples(duples);
}
terms.get(i).duples = duples;
terms.get(i).map = computeMap(terms.get(i), terms.get(i + 1));
}
// Finalize the output map, ditching the URI keys
Map resultMap = new HashMap();
for (Map map : terms.get(0).map.values()) {
for (Object key : map.keySet()) {
resultMap.put(key, map.get(key));
}
}
return resultMap;
}
/**
* Reverses the duples between two classes.
*
* @param duples
* @return
*/
private Map<URI, Set<URI>> reverseDuples(Map<URI, Set<URI>> duples) {
Map<URI, Set<URI>> reversed = new HashMap<URI, Set<URI>>();
for (URI uri : duples.keySet()) {
for (URI value : duples.get(uri)) {
if (!reversed.containsKey(value)) {
reversed.put(value, new HashSet<URI>());
}
reversed.get(value).add(uri);
}
}
return reversed;
}
/**
* Returns the path to traverse joins between term1 and term2.
* It may be from term1 to term2, or it may be from term2 to term1.
* It will start with the lowest indexed join class and work toward the highest.
*
* @param term1
* @param term2
* @return
*/
private List<JClass> computeJoinPath(MapBuilderTerm term1, MapBuilderTerm term2) {
List<JClass> joinPath = new ArrayList<JClass>();
JClass jc = null;
if (term2.jclass.index > term1.jclass.index) {
joinPath.add(term2.jclass);
String joinToAlias = term2.jclass.getJoinToAlias();
if (joinToAlias == null) {
throw new JoinerException(String.format(
"Cannot follow %s back to %s", term2.alias, term1.alias));
}
// Go backwards from our term to where term0 was computed
do {
jc = joiner.lookupAlias(joinToAlias);
if (jc == null) {
throw new JoinerException(String.format("Cannot find table for alias %s", joinToAlias));
}
joinPath.add(jc);
joinToAlias = jc.getJoinToAlias();
} while (joinToAlias != null && jc != term1.jclass);
} else {
joinPath.add(term1.jclass);
String joinToAlias = term1.jclass.getJoinToAlias();
// Go backwards from our term0 to where term was computed
do {
jc = joiner.lookupAlias(joinToAlias);
if (jc == null) {
throw new JoinerException(String.format("Cannot find table for alias %s", joinToAlias));
}
joinPath.add(jc);
joinToAlias = jc.getJoinToAlias();
} while (joinToAlias != null && jc != term2.jclass);
}
Collections.reverse(joinPath);
return joinPath;
}
private Map<URI, Set<URI>> computeDuples(List<JClass> joinPath) {
Map<URI, Set<URI>> duples = new HashMap<URI, Set<URI>>();
JClass jc = joinPath.get(0);
Set<URI> uris = jc.getUris();
for (URI key : uris) {
Set<URI> matchSet = iterateDuples(key, joinPath, 1);
if (matchSet != null && !matchSet.isEmpty()) {
duples.put(key, matchSet);
}
}
return duples;
}
private Set<URI> iterateDuples(URI key, List<JClass> joinPath, int joinPathIndex) {
// If this is the last JClass in the join list, just return
// the URI set determined by the key.
if ((joinPath.size() - 1) == joinPathIndex) {
Map<URI, Set<URI>> joinMap = joinPath.get(joinPathIndex).getJoinMap();
return joinMap.get(key);
}
// Otherwise, iterate through our values and recurse to next joinPath.
Set<URI> result = new HashSet<URI>();
Map<URI, Set<URI>> joinMap = joinPath.get(joinPathIndex).getJoinMap();
Set<URI> joinResults = joinMap.get(key);
if (joinResults != null) {
for (URI uri : joinResults) {
Set<URI> dupleURIs = iterateDuples(uri, joinPath, joinPathIndex + 1);
if (dupleURIs == null) {
continue;
}
result.addAll(dupleURIs);
}
}
return result;
}
/**
* Generates a Map<URI, Map<T1, T2>> where T1 and T2 are the expected return
* types for Term1 and Term2 and T1.id matches URI.
*
* @param term1
* @param term2
* @return
*/
private Map<URI, Map> computeMap(MapBuilderTerm term1, MapBuilderTerm term2) {
Map<URI, Map> outputMap = new HashMap<URI, Map>();
for (URI uri : term1.duples.keySet()) {
Map map = new HashMap();
Object object1 = getObject(term1.alias, uri, term1.type);
if (object1 == null) {
continue;
}
Object object2 = null;
object2 = getObject(term2.alias, term1.duples.get(uri), term2.type, term2.map);
if (object2 == null) {
continue;
}
map.put(object1, object2);
outputMap.put(uri, map);
}
return outputMap;
}
/**
* Return a single object based on type
*
* @param alias
* @param uri
* @param type
* @return
*/
private Object getObject(String alias, URI uri, MapBuilderTermType type) {
if (type == MapBuilderTermType.URI) {
return uri;
}
return joiner.find(alias, uri);
}
/**
* Return a collection object based on type
*
* @param alias
* @param uris
* @param type
* @param term2Map -- Map to be included as result of this map.
* @return
*/
private Object getObject(String alias, Set<URI> uris, MapBuilderTermType type, Map<URI, Map> term2Map) {
if (term2Map != null) {
Map resultMap = new HashMap();
for (Map map : term2Map.values()) {
for (Object key : map.keySet()) {
resultMap.put(key, map.get(key));
}
}
return resultMap;
}
if (type == MapBuilderTermType.URI) {
return uris;
}
if (type == MapBuilderTermType.LIST) {
ArrayList list = new ArrayList();
for (URI uri : uris) {
Object object = joiner.find(alias, uri);
if (object != null) {
list.add(object);
}
}
return list;
}
if (type == MapBuilderTermType.SET) {
HashSet set = new HashSet();
for (URI uri : uris) {
Object object = joiner.find(alias, uri);
if (object != null) {
set.add(object);
}
}
return set;
}
return null;
}
}