package com.cedarsoftware.ncube;
import com.cedarsoftware.ncube.exception.CoordinateNotFoundException;
import com.cedarsoftware.ncube.exception.RuleStop;
import com.cedarsoftware.ncube.formatters.HtmlFormatter;
import com.cedarsoftware.ncube.formatters.JsonFormatter;
import com.cedarsoftware.ncube.proximity.LatLon;
import com.cedarsoftware.ncube.proximity.Point2D;
import com.cedarsoftware.ncube.proximity.Point3D;
import com.cedarsoftware.util.ArrayUtilities;
import com.cedarsoftware.util.CaseInsensitiveMap;
import com.cedarsoftware.util.CaseInsensitiveSet;
import com.cedarsoftware.util.DateUtilities;
import com.cedarsoftware.util.DeepEquals;
import com.cedarsoftware.util.EncryptionUtilities;
import com.cedarsoftware.util.ReflectionUtils;
import com.cedarsoftware.util.StringUtilities;
import com.cedarsoftware.util.io.JsonObject;
import com.cedarsoftware.util.io.JsonReader;
import com.cedarsoftware.util.io.JsonWriter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
/**
* Implements an n-cube. This is a hyper (n-dimensional) cube
* of cells, made up of 'n' number of axes. Each Axis is composed
* of Columns that denote discrete nodes along an axis. Use NCubeManager
* to manage a list of NCubes. Documentation on Github.
*
* @author John DeRegnaucourt (jdereg@gmail.com)
* <br/>
* Copyright (c) Cedar Software LLC
* <br/><br/>
* Licensed 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
* <br/><br/>
* http://www.apache.org/licenses/LICENSE-2.0
* <br/><br/>
* 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.
*/
public class NCube<T>
{
private String name;
private final Map<String, Axis> axisList = new LinkedHashMap<String, Axis>();
final Map<Set<Column>, T> cells = new HashMap<Set<Column>, T>();
private T defaultCellValue;
private boolean ruleMode = false; // if true, throw exception if multiple cells are executed in more than one dimension
private transient final Map<String, Set<String>> scopeCache = new ConcurrentHashMap<String, Set<String>>();
private transient String version;
public static final String validCubeNameChars = "0-9a-zA-Z:.|#_-";
private static final String[] emptyStringArray = new String[] {};
public static final String RULE_EXEC_INFO = "_rule";
private static final ThreadLocal<Deque<StackEntry>> executionStack = new ThreadLocal<Deque<StackEntry>>()
{
public Deque<StackEntry> initialValue()
{
return new ArrayDeque();
}
};
private Map<String, Advice> advices = new LinkedHashMap<String, Advice>();
private Map<String, Object> metaProps = null;
/**
* @return Map (case insensitive keys) containing meta (additional) properties for the n-cube.
*/
public Map<String, Object> getMetaProperties()
{
Map ret = metaProps == null ? new CaseInsensitiveMap() : metaProps;
return Collections.unmodifiableMap(ret);
}
/**
* Set (add / overwrite) a Meta Property associated to this n-cube.
* @param key String key name of meta property
* @param value Object value to associate to key
* @return prior value associated to key or null if none was associated prior
*/
public Object setMetaProperty(String key, Object value)
{
if (metaProps == null)
{
metaProps = new CaseInsensitiveMap();
}
return metaProps.put(key, value);
}
/**
* Add a Map of meta properties all at once.
* @param allAtOnce Map of meta properties to add
*/
public void addMetaProperties(Map allAtOnce)
{
if (metaProps == null)
{
metaProps = new CaseInsensitiveMap();
}
metaProps.putAll(allAtOnce);
}
/**
* Remove all meta properties associated to this n-cube.
*/
public void clearMetaProperties()
{
if (metaProps != null)
{
metaProps.clear();
metaProps = null;
}
}
/**
* This is a "Pointer" (or Key) to a cell in an NCube.
* It consists of a String cube Name and a Set of
* Column references (one Column per axis).
*/
public static class StackEntry
{
final String cubeName;
final Map<String, Object> coord;
public StackEntry(String name, Map<String, Object> coordinate)
{
cubeName = name;
coord = coordinate;
}
public String toString()
{
StringBuilder s = new StringBuilder();
s.append(cubeName);
s.append(":{");
Iterator<Map.Entry<String, Object>> i = coord.entrySet().iterator();
while (i.hasNext())
{
Map.Entry<String, Object> coordinate = i.next();
s.append(coordinate.getKey());
s.append(':');
s.append(coordinate.getValue());
if (i.hasNext())
{
s.append(',');
}
}
s.append('}');
return s.toString();
}
}
/**
* Add advice to this n-cube that will be called before / after any Controller Method or
* URL-based Expression, for the given method
*/
void addAdvice(Advice advice, String method)
{
advices.put(advice.getName() + '/' + method, advice);
}
/**
* @return List<Advice> advices added to this n-cube.
*/
public List<Advice> getAdvices(String method)
{
List<Advice> result = new ArrayList();
method = '/' + method;
for (Map.Entry<String, Advice> entry : advices.entrySet())
{
// Entry key = "AdviceName/MethodName"
if (entry.getKey().endsWith(method))
{ // Entry.Value = Advice instance
result.add(entry.getValue());
}
}
return result;
}
/**
* For testing, advices need to be removed after test completes.
*/
void clearAdvices()
{
advices.clear();
}
/**
* Creata a new NCube instance with the passed in name
* @param name String name to use for the NCube.
*/
public NCube(String name)
{
if (name != null)
{ // If name is null, likely being instantiated via serialization
NCubeManager.validateCubeName(name);
}
this.name = name;
}
/**
* Stamp the version number on a loaded n-cube. This is so that when it is put into the
* NCubeManager cache, it can differentiate between two cubes with the same name but different
* version.
* @param ver String version (e.g. "1.0.1")
*/
void setVersion(String ver)
{
version = ver;
}
/**
* @return String version of this n-cube. The version is set when the n-cube is loaded by
* the NCubeManager.
*/
public String getVersion()
{
return version;
}
/**
* @return String name of the NCube
*/
public String getName()
{
return name;
}
/**
* Turn on/off the option to allow multiple
* @param on boolean true to turn on multi-cell execution, commonly used for
* Rule cubes.
*/
public void setRuleMode(boolean on)
{
ruleMode = on;
}
/**
* @return int total number of multiMatch axes on this ncube.
*/
public int getNumMultiMatchAxis()
{
int count = 0;
for (final Axis axis : axisList.values())
{
if (axis.isMultiMatch())
{
count++;
}
}
return count;
}
/**
* @return boolean state of multi-cell execute, 'true' if it is on, 'false' if off.
*/
public boolean getRuleMode()
{
return ruleMode;
}
/**
* Clear (remove) the cell at the given coordinate. The cell is dropped
* from the internal sparse storage.
* @param coordinate Map coordinate of Cell to remove.
* @return value of cell that was removed
*/
public T removeCell(final Map<String, Object> coordinate)
{
verifyNotMultiMode();
clearRequiredScopeCache();
return cells.remove(getCoordinateKey(validateCoordinate(coordinate)));
}
/**
* Clear a cell directly from the cell sparse-matrix specified by the passed in Column
* IDs. After this call, containsCell() for the same coordinate would return false.
*/
public T removeCellById(final Set<Long> coordinate)
{
clearRequiredScopeCache();
Set<Column> cols = new HashSet();
getColumnsAndCoordinateFromIds(coordinate, cols, null);
return cells.remove(cols);
}
/**
* @param coordinate Map (coordinate) of a cell
* @return boolean true if a cell has been mapped at the specified coordinate,
* false otherwise. If any of the axes have 'multiMatch' true, and there is more
* than one cell resolved, then this method will return true if any of the resolved
* cells exist (contain an actual cell at the given coordinate).
*/
public boolean containsCell(final Map<String, Object> coordinate)
{
return containsCell(coordinate, false);
}
/**
* @param coordinate Map (coordinate) of a cell
* @param all set to 'true' to make the return 'true' only if all resolved cells exist, otherwise
* set to 'false', and it will return 'true' if any of the resolved cells exist. If no resolved
* cells exist, this method will always return 'false'.
* @return boolean true if a cell has been mapped at the specified coordinate,
* false otherwise. If 'all' is true, then all resolved cells must exist. If 'all' is false,
* then only one resolved has to exist for it to return true. Boolean false is returned otherwise.
* If the cell is empty (not mapped) but the NCube defaultCellValue is set (not null), then this
* method will always return true.
*/
public boolean containsCell(final Map<String, Object> coordinate, final boolean all)
{
return containsCellValue(coordinate, all, true);
}
/**
* @param coordinate Map (coordinate) of a cell
* @param all set to 'true' to make the return 'true' only if all resolved cells exist, otherwise
* set to 'false', and it will return 'true' if any of the resolved cells exist. If no resolved
* cells exist, this method will always return 'false'.
* @return boolean true if a cell has been mapped at the specified coordinate,
* false otherwise. If 'all' is true, then all resolved cells must exist. If 'all' is false,
* then only one resolved has to exist for it to return true. Boolean false is returned otherwise.
* The NCube defaultCellValue has no effect on the return value of this method, unlike containsCell().
*/
public boolean containsCellValue(final Map<String, Object> coordinate, final boolean all)
{
return containsCellValue(coordinate, all, false);
}
/**
* @param coordinate Map (coordinate) of a cell
* @param all set to 'true' to make the return 'true' only if all resolved cells exist, otherwise
* set to 'false', and it will return 'true' if any of the resolved cells exist. If no resolved
* cells exist, this method will always return 'false'.
* @return boolean true if a cell has been mapped at the specified coordinate,
* false otherwise. If 'all' is true, then all resolved cells must exist. If 'all' is false,
* then only one resolved has to exist for it to return true. Boolean false is returned otherwise.
* The NCube defaultCellValue has no effect on the return value of this method, unlike containsCell().
*/
private boolean containsCellValue(final Map<String, Object> coordinate, final boolean all, boolean useDefault)
{
final Map<String, Object> validCoord = validateCoordinate(coordinate);
final Map<String, List<Column>> boundCoordinates = bindCoordinateToAxes(validCoord);
final String[] axisNames = getAxisNames(boundCoordinates);
final Map<String, Integer> counters = getCountersPerAxis(boundCoordinates);
final Set<Column> idCoord = new HashSet<Column>();
final Map<Long, Object[]> ruleExecutionCache = new HashMap<Long, Object[]>();
final Map<String, Integer> ruleAxisBindCount = new HashMap<String, Integer>();
boolean done = false;
boolean anyExist = false;
try
{
while (!done)
{
// Step #1 Create coordinate for current counter positions
idCoord.clear();
for (final String axisName : axisNames)
{
final List<Column> cols = boundCoordinates.get(axisName);
final Column boundColumn = cols.get(counters.get(axisName) - 1);
final Axis axis = axisList.get(axisName);
if (axis.getType() == AxisType.RULE)
{
Object ruleValue;
Object[] ruleValueHolder = ruleExecutionCache.get(boundColumn.id);
if (ruleValueHolder == null)
{ // Has the condition on the Rule axis been run this execution? If not, run it and cache it.
CommandCell cmd = (CommandCell) boundColumn.getValue();
Map executionContext = prepareExecutionContext(validCoord, new HashMap());
// cmd == null when on default column of a rule axis
ruleValue = cmd == null ? isZero(ruleAxisBindCount.get(axisName)) : cmd.execute(executionContext);
// Wrap rule return value, so that a rule that returns null is not mistaken for a cache miss.
ruleExecutionCache.put(boundColumn.id, new Object[]{ruleValue});
if (didRuleFire(ruleValue))
{ // Rule fired
Integer count = ruleAxisBindCount.get(axisName);
ruleAxisBindCount.put(axisName, count == null ? 1 : count + 1);
}
}
else
{
ruleValue = ruleValueHolder[0];
}
// A rule column on a given axis can be accessed more than once (example: A, B, C on
// one rule axis, X, Y, Z on another). This generates coordinate combinations
// (AX, AY, AZ, BX, BY, BZ, CX, CY, CZ). The condition columns must be run only once, on
// subsequent access, the cached result of the condition is used.
if (didRuleFire(ruleValue))
{
idCoord.add(boundColumn);
}
}
else
{
idCoord.add(boundColumn);
}
}
// Step #2 Execute cell and store return value, associating it to the Axes and Columns it bound to
if (idCoord.size() == axisNames.length)
{ // Conditions on rule axes that do not evaluate to true, do not generate complete coordinates (intentionally skipped)
if (useDefault && defaultCellValue != null)
{
return true;
}
boolean exists = cells.containsKey(idCoord);
if (all)
{
if (!exists)
{
return false;
}
}
else
{
if (exists)
{ // No need to continue testing for existence of further cells, we have 1 hit
anyExist = true;
break;
}
}
}
// Step #3 increment counters (variable radix increment)
done = incrementVariableRadixCount(counters, boundCoordinates, axisNames.length - 1, axisNames);
}
}
catch (RuleStop ignored)
{ // contains() does not process and output map that rule execution stats can be written to
}
checkForRuleModeViolation(ruleAxisBindCount);
return all || anyExist;
}
/**
* @return true if and only if there is a cell stored at the location
* specified by the Set<Long> coordinate. If the IDs don't locate a coordinate,
* no exception is thrown - simply false is returned.
*/
public boolean containsCellById(final Set<Long> coordinate)
{
Set<Column> cols = new HashSet();
getColumnsAndCoordinateFromIds(coordinate, cols, null);
return cells.containsKey(cols);
}
/**
* Store a value in the cell at the passed in coordinate.
* @param value A value to store in the NCube cell.
* @param coordinate Map coordinate used to identify what cell to update.
* The Map contains keys that are axis names, and values that will
* locate to the nereast column on the axis.
* @return the prior cells value.
*/
public T setCell(final T value, final Map<String, Object> coordinate)
{
verifyNotMultiMode();
clearRequiredScopeCache();
return cells.put(getCoordinateKey(validateCoordinate(coordinate)), value);
}
private void verifyNotMultiMode()
{
if (getNumMultiMatchAxis() > 0)
{
throw new IllegalStateException("Cannot use setCell()/removeCell() when NCube contains 1 or more 'multiMatch' axes, NCube '" + name + "'. Use setCellById() or removeCellById() instead.");
}
}
/**
* Set a cell directly into the cell sparse-matrix specified by the passed in
* Column IDs.
*/
public T setCellById(final T value, final Set<Long> coordinate)
{
clearRequiredScopeCache();
Set<Column> cols = new HashSet();
getColumnsAndCoordinateFromIds(coordinate, cols, null);
return cells.put(cols, value);
}
/**
* Clear the require scope caches. This is required when a cell, column, or axis
* changes.
*/
private void clearRequiredScopeCache()
{
synchronized(scopeCache)
{
scopeCache.clear();
}
}
/**
* Use the passed in object as a 'Map' coordinate, where the field
* names of the class are the keys (axis names) and the values bind
* to the column for the given axis. Set the cell at this location
* to the value (T) passed in.
* @param value A value to store in the NCube cell.
* @param o Object any Java object to bind to an NCube.
* @return the prior cells value.
*/
public T setCellUsingObject(final T value, final Object o)
{
return setCell(value, objectToMap(o));
}
/**
* Mainly useful for displaying an ncube within an editor. This will
* get the actual stored cell, not execute it. The caller will get
* CommandCell instances for example, as opposed to the return value
* of the executed CommandCell.
*/
public T getCellByIdNoExecute(final Set<Long> coordinate)
{
Set<Column> cols = new HashSet();
getColumnsAndCoordinateFromIds(coordinate, cols, null);
return cells.get(cols);
}
/**
* @param coordinate Map of axis names to single values to match against the
* NCube. See getCell(Map coordinate, Map input) for more description.
* @return Cell pinpointed by the input coordinate (it is executed before
* being returned). In the case of a value, this means nothing. In the case
* of a CommandCell, the CommandCell is executed.
*/
public T getCell(final Map<String, Object> coordinate)
{
return getCell(coordinate, new HashMap());
}
/**
* This version of getCell() allows the code executing within ncube
* to write to the supplied output Map.
* NCube. The cell pinpointed by this input coordinate is returned. If the cell
* is a 'command cell', then the entire cell command is run and once it completes
* execution, the return of the execution will be the return value for the cell.
* If one or more of the axes are in multiMatch mode, which will resolve to more
* than one cell, but only one of the cells 'contains' a value, this API will still
* return the single value. If more than one cell has an established value, it will
* throw an exception indicating the ambiguity. If you need to resolve to multiple
* cells, and that is OK, then call getCells(), not getCell().
* @return Cell pinpointed by the input coordinate.
*/
public T getCell(final Map<String, Object> coordinate, final Map output)
{
final Map<Map<String, Column>, T> hits = getCells(coordinate, output);
T cellContent = null;
int count = 0;
final Set<Column> coord = new HashSet<Column>();
for (final Map.Entry<Map<String, Column>, T> entry : hits.entrySet())
{
coord.clear();
coord.addAll(entry.getKey().values());
if (cells.containsKey(coord))
{
cellContent = entry.getValue();
count++;
if (count > 1)
{
String json = "";
try
{
json = JsonWriter.objectToJson(hits);
}
catch (IOException e)
{
json = "unable to make return value in JSON string";
}
throw new IllegalStateException("getCell() coordinate resolved to " + count +
" non-empty cells. Either call getCells() which allows multi-cell return, or fix the overlap in your Column definitions, NCube '" +
name + "'. Return value:\n" + json);
}
}
}
if (count == 0)
{
cellContent = defaultCellValue;
}
// Only 1 return value, so this always works.
return cellContent;
}
/**
* Use the passed in object as a 'Map' coordinate, where the field
* names of the class are the keys (axis names) and the values bind
* to the column for the given axis.
* @param o Object any Java object to bind to an NCube.
* @return Cell pinpointed by the input coordinate.
*/
public T getCellUsingObject(final Object o)
{
return getCell(objectToMap(o));
}
/**
* Use the passed in object as a 'Map' coordinate, where the field
* names of the class are the keys (axis names) and the values bind
* to the column for the given axis.
* @param o Object any Java object to bind to an NCube.
* @param output Map which can be written to by code that executes within ncube cells.
* @return Cell pinpointed by the input coordinate.
*/
public T getCellUsingObject(final Object o, Map output)
{
return getCell(objectToMap(o), output);
}
/**
* The lowest level cell fetch. This method uses the Set<Column> to fetch an
* exact cell, while maintaining the original input coordinate that the location
* was derived from (required because a given input coordinate could map to more
* than one cell). Once the cell is located, it is executed and the value from
* the executed cell is returned (in the case of Command Cells, it is the return
* value of the execution, otherwise the return is the value stored in the cell,
* and if there is no cell, the defaultCellValue from NCube is returned, if one
* is set.
* REQUIRED: The coordinate passed to this method must have already been run
* through validateCoordinate(), which duplicates the coordinate and ensures the
* coordinate has at least an entry for each axis.
*/
T getCellById(final Set<Column> idCoord, final Map<String, Object> coordinate, final Map output)
{
// First, get a ThreadLocal copy of an NCube execution stack
Deque<StackEntry> stackFrame = executionStack.get();
boolean pushed = false;
try
{
// Form fully qualified cell lookup (NCube name + coordinate)
// Add fully qualified coordinate to ThreadLocal execution stack
final StackEntry entry = new StackEntry(name, coordinate);
stackFrame.push(entry);
pushed = true;
final T retVal = executeCellById(idCoord, coordinate, output);
return retVal; // split into 2 statements for debugging
}
finally
{ // Unwind stack: always remove if stacked pushed, even if Exception has been thrown
if (pushed)
{
stackFrame.pop();
}
}
}
/**
* Execute the referenced cell. If the cell is a value, it will be returned.
* If the cell is a CommandCell, then it will be executed. That allows the
* cell to further access 'this' ncube or other NCubes within the NCubeManager,
* providing significant power and capability, as it each execution is effectively
* a new 'Decision' within a decision tree. Further more, because ncube supports
* Groovy code within cells, a cell, when executing may perform calculations,
* programmatic execution within the cell (looping, conditions, modifications),
* as well as referencing back into 'this' or other ncubes. The output map passed
* into this method allows the executing cell to write out information that can be
* accessed after the execution completes, or even during execution, as a parameter
* passing.
* @return T ultimate value reached by executing the contents of this cell.
* If the passed in coordinate refers to a non-command cell, then the value
* of that cell is returned, otherwise the command in the cell is executed,
* resulting in recursion that will ultimately end when a non-command cell
* is reached.
*/
private T executeCellById(final Set<Column> idCoord, final Map<String, Object> coord, final Map output)
{
// Get internal representation of a coordinate (a Set of Column identifiers.)
T cellValue = cells.containsKey(idCoord) ? cells.get(idCoord) : defaultCellValue;
Map<String, Object> ctx = prepareExecutionContext(coord, output);
try
{
if (cellValue instanceof CommandCell)
{
return (T) ((CommandCell) cellValue).execute(ctx);
}
else
{
return (T) cellValue;
}
}
catch (CoordinateNotFoundException e)
{
throw new CoordinateNotFoundException("Coordinate not found in NCube '" + name + "'\n" + stackToString(), e);
}
catch (Exception e)
{
throw new RuntimeException("Error occurred executing CommandCell in NCube '" + name + "'\n" + stackToString(), e);
}
}
/**
* Prepare the execution context by providing it with references to
* important items like the input coordinate, output map, stack,
* this (ncube), and the NCubeManager.
*/
public Map<String, Object> prepareExecutionContext(final Map<String, Object> coord, final Map output)
{
final Map<String, Object> args = new HashMap<String, Object>();
args.put("input", coord); // Input coordinate is already a duplicate (CaseInsensitiveMap) at this point
args.put("output", output);
args.put("ncube", this);
return args;
}
/**
* Get a Map of column values and corresponding cell values where all axes
* but one are held to a fixed (single) column, and one axis allows more than
* one value to match against it.
* @param coordinate Map - A coordinate where the keys are axis names, and the
* values are intended to match a column on each axis, with one exception. One
* of the axis values in the coordinate input map must be an instanceof a Set.
* If the set is empty, all columns and cell values for the given axis will be
* returned in a Map. If the Set has values in it, then only the columns
* on the 'wildcard' axis that match the values in the set will be returned (along
* with the corresponding cell values).
* @return a Map containing Axis names and values to bind to those axes. One of the
* axes must have a Set bound to it.
*/
public Map<Object, T> getMap(final Map<String, Object> coordinate)
{
final Map<String, Object> coord = validateCoordinate(coordinate);
final Axis wildcardAxis = getWildcardAxis(coord);
final List<Column> columns = getWildcardColumns(wildcardAxis, coord);
final Map<Object, T> result = new HashMap<Object, T>();
final String axisName = wildcardAxis.getName();
for (final Column column : columns)
{
coord.put(axisName, column.getValueThatMatches());
result.put(column.getValue(), getCell(coord));
}
return result;
}
/**
* Return all cells that match the given input coordinate.
* NCube axes. The cell(s) pinpointed by this input coordinate are returned. If the
* cell is a 'command cell', then the CommandCell is execute and once it completes
* execution, the return of the execution will be the return value for the cell.
* @return a Map, where the keys of the Map are coordinates (Map<String, Object>), and
* the associated value is the executed cell value for the given coordinate.
*/
public Map<Map<String, Column>, T> getCells(final Map<String, Object> coordinate, final Map output)
{
final Map<String, Object> validCoord = validateCoordinate(coordinate);
final Map<String, List<Column>> boundCoordinates = bindCoordinateToAxes(validCoord);
final String[] axisNames = getAxisNames(boundCoordinates);
final Map<Map<String, Column>, T> executedCells = new LinkedHashMap<Map<String, Column>, T>();
final Map<String, Integer> counters = getCountersPerAxis(boundCoordinates);
final Set<Column> idCoord = new HashSet<Column>();
final Map<Long, Object[]> ruleColExecutionValue = new HashMap<Long, Object[]>();
final Map<String, Integer> ruleAxisBindCount = new HashMap<String, Integer>();
final Map<RuleMetaKeys, Object> ruleInfo = new CaseInsensitiveMap<RuleMetaKeys, Object>();
boolean done = false;
boolean anyRuleAxes = false;
try
{
while (!done)
{
// Step #1 Create coordinate for current counter positions
final Map<String, Column> coord = new CaseInsensitiveMap<String, Column>();
idCoord.clear();
for (final String axisName : axisNames)
{
final List<Column> cols = boundCoordinates.get(axisName);
final Column boundColumn = cols.get(counters.get(axisName) - 1);
final Axis axis = axisList.get(axisName);
if (axis.getType() == AxisType.RULE)
{
anyRuleAxes = true;
Object ruleValue;
Object[] ruleValueHolder = ruleColExecutionValue.get(boundColumn.id);
if (ruleValueHolder == null)
{ // Has the condition on the Rule axis been run this execution? If not, run it and cache it.
CommandCell cmd = (CommandCell) boundColumn.getValue();
Map ctx = prepareExecutionContext(validCoord, output);
// If the cmd == null, then we are looking at a default column on a rule axis.
ruleValue = cmd == null ? isZero(ruleAxisBindCount.get(axisName)) : cmd.execute(ctx);
ruleColExecutionValue.put(boundColumn.id, new Object[]{ruleValue});
if (didRuleFire(ruleValue))
{ // Rule fired
Integer count = ruleAxisBindCount.get(axisName);
ruleAxisBindCount.put(axisName, count == null ? 1 : count + 1);
}
}
else
{
ruleValue = ruleValueHolder[0];
}
// A rule column on a given axis can be accessed more than once (example: A, B, C on
// one rule axis, X, Y, Z on another). This generates coordinate combinations
// (AX, AY, AZ, BX, BY, BZ, CX, CY, CZ). The condition columns must be run only once, on
// subsequent access, the cached result of the condition is used.
if (didRuleFire(ruleValue))
{
coord.put(axisName, boundColumn);
idCoord.add(boundColumn);
}
}
else
{
coord.put(axisName, boundColumn);
idCoord.add(boundColumn);
}
}
// Step #2 Execute cell and store return value, associating it to the Axes and Columns it bound to
if (idCoord.size() == axisNames.length)
{ // Conditions on rule axes that do not evaluate to true, do not generate complete coordinates (intentionally skipped)
T cellValue = getCellById(idCoord, validCoord, output);
executedCells.put(coord, cellValue);
}
// Step #3 increment counters (variable radix increment)
done = incrementVariableRadixCount(counters, boundCoordinates, axisNames.length - 1, axisNames);
}
ruleInfo.put(RuleMetaKeys.RULE_STOP, false);
}
catch (RuleStop e)
{
ruleInfo.put(RuleMetaKeys.RULE_STOP, true);
}
ruleInfo.put(RuleMetaKeys.NUM_RESOLVED_CELLS, (long) executedCells.size());
if (anyRuleAxes)
{
output.put(NCube.RULE_EXEC_INFO, ruleInfo);
}
checkForRuleModeViolation(ruleAxisBindCount);
return executedCells;
}
private static boolean didRuleFire(Object ruleValue)
{
if (ruleValue instanceof Number)
{
return !ruleValue.equals(0);
}
else
{
return ruleValue != Boolean.FALSE && ruleValue != null;
}
}
private static boolean isZero(Integer count)
{
return count == null || count == 0;
}
/**
* If ruleMode is active, then throw an exception if, and only if, two or more axes
* bind to two or more columns. Only 1-axis is allowed to bind to multiple columns
* if ruleMode is set.
* @param ruleAxisBindCount Map of AxisName to Integer count of the number of columns
* that bound to the given axis.
*/
private void checkForRuleModeViolation(Map<String, Integer> ruleAxisBindCount)
{
if (ruleMode)
{
int axesWithMultipleBindings = 0;
for (Integer count : ruleAxisBindCount.values())
{
if (count > 1)
{
axesWithMultipleBindings++;
if (axesWithMultipleBindings > 1)
{
throw new IllegalStateException("Multiple rule axes had 2 or more conditions fire, NCube '" + getName() + "', rule axes counts: " + ruleAxisBindCount.entrySet());
}
}
}
}
}
/**
* Bind the input coordinate to each axis. The reason the column is a List of columns that the coordinate
* binds to on the axis, is to support multiMatch and RULE axes. On a regular axis, the coordinate binds
* to a column (with a binary search or hashMap lookup), however, on a multiMatch or RULE axis, the act
* of binding to an axis results in a List<Column>.
* @param coord The passed in input coordinate to bind (or multi-bind) to each axis.
*/
private Map<String, List<Column>> bindCoordinateToAxes(Map coord)
{
Map<String, List<Column>> coordinates = new HashMap<String, List<Column>>();
for (final Map.Entry<String, Axis> entry : axisList.entrySet())
{
final String axisNameLowcase = entry.getKey();
final Axis axis = entry.getValue();
final Comparable value = (Comparable) coord.get(axisNameLowcase);
if (axis.getType() == AxisType.RULE)
{ // For RULE axis, all possible columns must be added (they are tested later during execution)
coordinates.put(axisNameLowcase, axis.getColumns());
}
else if (axis.isMultiMatch())
{ // For a multiMatch axis, only the columns that match the input coordinate are returned
List<Column> cols = axis.findColumns(value);
if (cols == null || cols.size() < 1)
{
throw new CoordinateNotFoundException("Value '" + value + "' not found on multi-match axis '" + axis.getName() + "', NCube '" + name + "'");
}
coordinates.put(axisNameLowcase, cols);
}
else
{ // Find the single column that binds to the input coordinate on a regular axis.
final Column column = axis.findColumn(value);
if (column == null)
{
throw new CoordinateNotFoundException("Value '" + value + "' not found on axis '" + axis.getName() + "', NCube '" + name + "'");
}
List<Column> cols = new ArrayList<Column>();
cols.add(column);
coordinates.put(axisNameLowcase, cols);
}
}
return coordinates;
}
private static String[] getAxisNames(final Map<String, List<Column>> bindings)
{
return bindings.keySet().toArray(emptyStringArray);
}
private static Map<String, Integer> getCountersPerAxis(final Map<String, List<Column>> bindings)
{
final Map<String, Integer> counters = new HashMap<String, Integer>();
// Set counters to 1
for (final String axisName : bindings.keySet())
{
counters.put(axisName, 1);
}
return counters;
}
/**
* Given a Set of column identifiers, return a Set of Columns. This method
* ensures that enough column identifiers are passed in (at least 1 per each
* axis), unless an axis has a default, in which case the default column
* will be used. Additionally, it verifies that these ids are truly ids of
* columns on an axis.
* @return Set<Column> that represent the columns identified by the passed in
* set of Longs.
* @throws IllegalArgumentException if not enough IDs are passed in, or an axis
* cannot bind to any of the passed in IDs.
*/
public void getColumnsAndCoordinateFromIds(final Set<Long> coordinate, Set<Column> cols, Map<String, Object> coord)
{
// Ensure that the specified coordinate matches a column on each axis
final Set<String> axisNamesRef = new CaseInsensitiveSet<String>();
final Set<String> allAxes = new CaseInsensitiveSet<String>(axisList.keySet());
// Bind all Longs to Columns on an axis. Allow for additional columns to be specified,
// but not more than one column ID per axis. Also, too few can be supplied, if and
// only if, the axes that are not bound too have a Default column (which will be chosen).
for (final Axis axis : axisList.values())
{
final String axisName = axis.getName();
for (final Long id : coordinate)
{
Column column = axis.getColumnById(id);
if (column != null)
{
if (axisNamesRef.contains(axisName))
{
throw new IllegalArgumentException("Cannot have more than one column ID per axis, axis '" + axisName + "', NCube '" + name + "'");
}
axisNamesRef.add(axisName);
if (cols != null)
{
cols.add(column);
}
if (coord != null)
{
coord.put(axisName, column.getValueThatMatches());
}
}
}
}
// Remove the referenced axes from allAxes set. This leaves axes to be resolved.
allAxes.removeAll(axisNamesRef);
// For the unbound axes, bind them to the Default Column (if the axis has one)
axisNamesRef.clear(); // use Set again, this time to hold unbound axes
axisNamesRef.addAll(allAxes);
// allAxes at this point, is the unbound axis (not referenced by an id in input coordinate)
for (final String axisName : allAxes)
{
Axis axis = getAxis(axisName);
if (axis.hasDefaultColumn())
{
Column defCol = axis.getDefaultColumn();
axisNamesRef.remove(axisName);
if (cols != null)
{
cols.add(defCol);
}
if (coord != null)
{
coord.put(axisName, defCol.getValueThatMatches());
}
}
}
if (!axisNamesRef.isEmpty())
{
throw new IllegalArgumentException("Column IDs missing for the axes: " + axisNamesRef + ", NCube '" + name + "'");
}
}
private static List<StackEntry> getStackAsList()
{
return new ArrayList<StackEntry>(executionStack.get());
}
private Map<String, Object> objectToMap(final Object o)
{
if (o == null)
{
throw new IllegalArgumentException("null is not allowed as an input coordinate, NCube '" + name + "'\n" + stackToString());
}
try
{
final Collection<Field> fields = ReflectionUtils.getDeepDeclaredFields(o.getClass());
final Iterator<Field> i = fields.iterator();
final Map<String, Object> newCoord = new CaseInsensitiveMap<String, Object>();
while (i.hasNext())
{
final Field field = i.next();
final String fieldName = field.getName();
final Object fieldValue = field.get(o);
if (newCoord.containsKey(fieldName))
{ // This can happen if field name is same between parent and child class (dumb, but possible)
newCoord.put(field.getDeclaringClass().getName() + '.' + fieldName, fieldValue);
}
else
{
newCoord.put(fieldName, fieldValue);
}
}
return newCoord;
}
catch (Exception e)
{
throw new RuntimeException("Failed to access field of passed in object, NCube '" + name + "'\n" + stackToString(), e);
}
}
private static String stackToString()
{
final Deque<StackEntry> stack = executionStack.get();
final Iterator<StackEntry> i = stack.descendingIterator();
final StringBuilder s = new StringBuilder();
while (i.hasNext())
{
final StackEntry key = i.next();
s.append("-> cell:");
s.append(key.toString());
if (i.hasNext())
{
s.append('\n');
}
}
return s.toString();
}
/**
* Increment the variable radix number passed in. The number is represented by a Map, where the keys are the
* digit names (axis names), and the values are the associated values for the number.
* @return false if more incrementing can be done, otherwise true.
*/
private static boolean incrementVariableRadixCount(final Map<String, Integer> counters,
final Map<String, List<Column>> bindings,
int digit, final String[] axisNames)
{
while (true)
{
final int count = counters.get(axisNames[digit]);
final List<Column> cols = bindings.get(axisNames[digit]);
if (count >= cols.size())
{ // Reach max value for given dimension (digit)
if (digit == 0)
{ // we have reached the max radix for the most significant digit - we are done
return true;
}
counters.put(axisNames[digit--], 1);
}
else
{
counters.put(axisNames[digit], count + 1); // increment counter
return false;
}
}
}
/**
* @param coordinate passed in coordinate for accessing this n-cube
* @return Axis the axis that has a Set specified for it rather than a non-Set value.
* The Set associated to the input coordinate field indicates that the caller is
* matching more than one value against this axis.
*/
private Axis getWildcardAxis(final Map<String, Object> coordinate)
{
int count = 0;
Axis wildcardAxis = null;
for (Map.Entry<String, Object> entry : coordinate.entrySet())
{
if (entry.getValue() instanceof Set)
{
count++;
wildcardAxis = axisList.get(entry.getKey().toLowerCase()); // intentional case insensitive match
}
}
if (count == 0)
{
throw new IllegalArgumentException("No 'Set' value found within input coordinate, NCube '" + name + "'");
}
if (count > 1)
{
throw new IllegalArgumentException("More than one 'Set' found as value within input coordinate, NCube '" + name + "'");
}
return wildcardAxis;
}
/**
* @param coordinate Map containing Axis names as keys, and Comparable's as
* values. The coordinate key matches an axis name, and then the column on the
* axis is found that best matches the input coordinate value.
* @return a Set key in the form of Column1,Column2,...Column-n where the Columns
* are the Columns along the axis that match the value associated to the key (axis
* name) of the passed in input coordinate. The ordering is the order the axes are
* stored within in NCube. The returned Set is the 'key' of NCube's cells Map, which
* maps a coordinate (Set of column pointers) to the cell value.
*/
private Set<Column> getCoordinateKey(final Map<String, Object> coordinate)
{
final Set<Column> key = new HashSet<Column>();
for (final Map.Entry<String, Axis> entry : axisList.entrySet())
{
final Axis axis = entry.getValue();
final Object value = coordinate.get(entry.getKey());
final Column column = axis.findColumn((Comparable) value);
if (column == null)
{
throw new CoordinateNotFoundException("Value '" + value + "' not found on axis '" + axis.getName() + "', NCube '" + name + "'");
}
key.add(column);
}
return key;
}
/**
* Ensure that the Map coordinate dimensionality satisfies this nCube.
* This method verifies that all axes are listed by name in the input coordinate.
* It should be noted that if the input coordinate contains the axis names with
* exact case match, this method performs much faster. It must make a second
* pass through the axis list when the input coordinate axis names do not match
* the case of the axis.
*/
private Map<String, Object> validateCoordinate(final Map<String, Object> coordinate)
{
if (coordinate == null)
{
throw new IllegalArgumentException("'null' passed in for coordinate Map, NCube '" + name + "'");
}
if (coordinate.isEmpty())
{
throw new IllegalArgumentException("Coordinate Map must have at least one coordinate, NCube '" + name + "'");
}
// Duplicate input coordinate
final Map<String, Object> copy = new CaseInsensitiveMap<String, Object>(coordinate);
for (Map.Entry<String, Axis> entry : axisList.entrySet())
{
final String key = entry.getKey();
final Axis axis = entry.getValue();
if (!copy.containsKey(key) && axis.getType() != AxisType.RULE)
{
StringBuilder keys = new StringBuilder();
Iterator<String> i = coordinate.keySet().iterator();
while (i.hasNext())
{
keys.append("'");
keys.append(i.next());
keys.append("'");
if (i.hasNext())
{
keys.append(", ");
}
}
throw new IllegalArgumentException("Input coordinate with axes (" + keys + ") does not contain a coordinate for axis '" + axis.getName() + "' required for NCube '" + name + "'");
}
final Object value = copy.get(key);
if (value != null)
{
if (!(value instanceof Comparable) && !(value instanceof Set))
{
throw new IllegalArgumentException("Coordinate value '" + value + "' must be of type 'Comparable' (or a Set) to bind to axis '" + axis.getName() + "' on NCube '" + name + "'");
}
}
}
return copy;
}
/**
* @param coordinate Map containing Axis names as keys, and Comparable's as
* values. The coordinate key matches an axis name, and then the column on the
* axis is found that best matches the input coordinate value. The input coordinate
* must contain one Set as a value for one of the axes of the NCube. If empty,
* then the Set is treated as '*' (star). If it has 1 or more elements in
* it, then for each entry in the Set, a column position value is returned.
*
* @return a List of all columns that match the values in the Set, or in the
* case of an empty Set, all columns on the axis.
*/
private List<Column> getWildcardColumns(final Axis wildcardAxis, final Map<String, Object> coordinate)
{
final List<Column> columns = new ArrayList<Column>();
final Set<Comparable> wildcardSet = (Set<Comparable>) coordinate.get(wildcardAxis.getName());
// This loop grabs all the columns from the axis which match the values in the Set
for (final Comparable value : wildcardSet)
{
final Column column = wildcardAxis.findColumn(value);
if (column == null)
{
throw new CoordinateNotFoundException("Value '" + value + "' not found using Set on axis '" + wildcardAxis.getName() + "', NCube '" + name + "'");
}
columns.add(column);
}
// To support '*', an empty Set is bound to the axis such that all columns are returned.
if (wildcardSet.isEmpty())
{
columns.addAll(wildcardAxis.getColumns());
}
return columns;
}
/**
* @return T the default value that will be returned when a coordinate specifies
* a cell that has no entry associated to it. This is a space-saving technique,
* as the most common cell value can be set as the defaultCellValue, and then the
* cells that would have had this value can be left empty.
*/
public T getDefaultCellValue()
{
return defaultCellValue;
}
/**
* Set the default cell value for this n-cube. This is a space-saving technique,
* as the most common cell value can be set as the defaultCellValue, and then the
* cells that would have had this value can be left empty.
* @param defaultCellValue T the default value that will be returned when a coordinate
* specifies a cell that has no entry associated to it.
*/
public void setDefaultCellValue(final T defaultCellValue)
{
this.defaultCellValue = defaultCellValue;
}
/**
* Clear all cell values. All axes and columns remain.
*/
public void clearCells()
{
cells.clear();
}
/**
* Add a column to the n-cube
* @param axisName String name of the Axis to which the column will be added.
* @param value Comparable that will be the value for the given column. Cannot be null.
* @return Column the added Column.
*/
public Column addColumn(final String axisName, final Comparable value)
{
final Axis axis = getAxis(axisName);
if (axis == null)
{
throw new IllegalArgumentException("Could not add column. Axis name '" + axisName + "' was not found on NCube '" + name + "'");
}
Column newCol = axis.addColumn(value);
clearRequiredScopeCache();
return newCol;
}
/**
* Delete a column from the named axis. All cells that reference this
* column will be deleted.
* @param axisName String name of Axis contains column to be removed.
* @param value Comparable value used to identify column
* @return boolean true if deleted, false otherwise
*/
public boolean deleteColumn(final String axisName, final Comparable value)
{
final Axis axis = getAxis(axisName);
if (axis == null)
{
throw new IllegalArgumentException("Could not delete column. Axis name '" + axisName + "' was not found on NCube '" + name + "'");
}
clearRequiredScopeCache();
final Column column = axis.deleteColumn(value);
if (column == null)
{
return false;
}
// Remove all cells that reference the deleted column
final Iterator<Set<Column>> i = cells.keySet().iterator();
while (i.hasNext())
{
final Set<Column> key = i.next();
// Locate the uniquely identified column, regardless of axis order
if (key.contains(column))
{
i.remove();
}
}
return true;
}
/**
* Move the column indicated by curPos to the newPos along the axis specified by name.
* Note this only works for an axis in display order. This method will through an
* IllegalStateException if you attempt to call it on an axis in Sorted order. If the
* columns indicated by curPos or newPos do not exist, an IllegalArgumentException
* will be thrown.
*/
public boolean moveColumn(final String axisName, final int curPos, final int newPos)
{
final Axis axis = getAxis(axisName);
if (axis == null)
{
throw new IllegalArgumentException("Could not move column. Axis name '" + axisName + "' was not found on NCube '" + name + "'");
}
return axis.moveColumn(curPos, newPos);
}
/**
* Change the value of a Column along an axis.
* @param id long indicates the column to change
* @param value Comparable new value to set into the column
*/
public void updateColumn(long id, Comparable value)
{
Axis axis = getAxisFromColumnId(id);
if (axis == null)
{
throw new IllegalArgumentException("No column exists with the id " + id + " within NCube '" + name + "'");
}
axis.updateColumn(id, value);
}
/**
* Update all of the columns along an axis at once. Any cell referencing a column that
* is deleted, will also be deleted from the internal sparse matrix (Map) of cells.
* @param newCols Axis used only as a Column holder, such the columns within this
* Axis are in display order as would come in from a UI, for example.
* @return Set<Long> column ids, indicating which columns were deleted.
*/
public Set<Long> updateColumns(final Axis newCols)
{
if (newCols == null)
{
throw new IllegalArgumentException("Cannot pass in null Axis for updating columns, NCube '" + name + "'");
}
final String lowAxisName = newCols.getName().toLowerCase();
if (!axisList.containsKey(lowAxisName))
{
throw new IllegalArgumentException("No axis exists with the name: " + newCols.getName() + ", NCube '" + name + "'");
}
final Axis axisToUpdate = axisList.get(newCols.getName().toLowerCase());
final Set<Long> colsToDel = axisToUpdate.updateColumns(newCols);
Column testColumn = new Column(1);
Iterator<Set<Column>> i = cells.keySet().iterator();
while (i.hasNext())
{
Set<Column> cols = i.next();
for (Long id : colsToDel)
{
testColumn.setId(id);
if (cols.contains(testColumn))
{ // If cell referenced deleted column, drop the cell
i.remove();
break;
}
}
}
return colsToDel;
}
/**
* Given the passed in Column ID, return the axis that contains the column.
* @param id Long id of a Column on one of the Axes within this n-cube.
* @return Axis containing the column id, or null if the id does not match
* any columns.
*/
public Axis getAxisFromColumnId(long id)
{
for (final Axis axis : axisList.values())
{
if (axis.idToCol.containsKey(id))
{
return axis;
}
}
return null;
}
/**
* @return int total number of cells that are uniquely set (non default)
* within this NCube.
*/
public int getNumCells()
{
return cells.size();
}
/**
* @return read-only copy of the n-cube cells.
*/
public Map<Set<Column>, T> getCellMap()
{
return Collections.unmodifiableMap(cells);
}
/**
* Retrieve an axis (by name) from this NCube.
* @param axisName String name of Axis to fetch.
* @return Axis instance requested by name, or null
* if it does not exist.
*/
public Axis getAxis(final String axisName)
{
return axisList.get(axisName.toLowerCase());
}
/**
* Add an Axis to this NCube.
* All cells will be cleared when axis is added.
* @param axis Axis to add
*/
public void addAxis(final Axis axis)
{
String axisName = axis.getName().toLowerCase();
if (axisList.containsKey(axisName))
{
throw new IllegalArgumentException("An axis with the name '" + axis.getName()
+ "' already exists on NCube '" + name + "'");
}
cells.clear();
axisList.put(axisName, axis);
clearRequiredScopeCache();
}
public void renameAxis(final String oldName, final String newName)
{
if (StringUtilities.isEmpty(oldName) || StringUtilities.isEmpty(newName))
{
throw new IllegalArgumentException("Axis name cannot be empty or blank");
}
if (getAxis(newName) != null)
{
throw new IllegalArgumentException("There is already an axis named '" + oldName + "' on NCube '" + name + "'");
}
final Axis axis = getAxis(oldName);
if (axis == null)
{
throw new IllegalArgumentException("Axis '" + oldName + "' not on NCube '" + name + "'");
}
axisList.remove(oldName.toLowerCase());
axis.setName(newName);
axisList.put(newName.toLowerCase(), axis);
}
/**
* Remove an axis from an NCube.
* All cells will be cleared when an axis is deleted.
* @param axisName String name of axis to remove
* @return boolean true if removed, false otherwise
*/
public boolean deleteAxis(final String axisName)
{
cells.clear();
clearRequiredScopeCache();
return axisList.remove(axisName.toLowerCase()) != null;
}
public int getNumDimensions()
{
return axisList.size();
}
public List<Axis> getAxes()
{
return new ArrayList<Axis>(axisList.values());
}
/**
* Determine the required 'scope' needed to access all cells within this
* NCube. Effectively, you are determining how many axis names (keys in
* a Map coordinate) are required to be able to access any cell within this
* NCube. Keep in mind, that CommandCells allow this NCube to reference
* other NCubes and therefore the referenced NCubes must be checked as
* well. This code will not get stuck in an infinite loop if one cube
* has cells that reference another cube, and it has cells that reference
* back (it has cycle detection).
* @return Set<String> names of axes that will need to be in an input coordinate
* in order to use all cells within this NCube.
*/
public Set<String> getRequiredScope()
{
if (scopeCache.containsKey(name))
{ // Cube name to requiredScopeKeys map
return new CaseInsensitiveSet<String>(scopeCache.get(name)); // return modifiable copy (sorted order maintained)
}
synchronized (scopeCache)
{
if (scopeCache.containsKey(name))
{ // Check again in case more than one thread was waiting for the cached answer to be built.
return new CaseInsensitiveSet<String>(scopeCache.get(name)); // return modifiable copy (sorted order maintained)
}
final Set<String> requiredScope = new CaseInsensitiveSet<String>();
final LinkedList<NCube> stack = new LinkedList<NCube>();
final Set<String> visited = new HashSet<String>();
stack.addFirst(this);
while (!stack.isEmpty())
{
final NCube<?> cube = stack.removeFirst();
final String cubeName = cube.getName();
if (visited.contains(cubeName))
{
continue;
}
visited.add(cubeName);
for (final Axis axis : cube.axisList.values())
{ // Use original axis name (not .toLowerCase() version)
if (axis.getType() != AxisType.RULE)
{
requiredScope.add(axis.getName());
}
}
// Snag all input.variable references from CommandCells ('variable' is a potential required scope)
for (String key : getScopeKeysFromCommandCells(cube.cells))
{
requiredScope.add(key);
}
// Snag all input.variable references from Rule axis conditions ('variable' is a potential required scope)
for (String key : getScopeKeysFromRuleAxes(cube))
{
requiredScope.add(key);
}
// Add all referenced sub-cubes to the stack (locate n-cube references @cube[:], $cube[:],
// and NCubeManager.getCube('name'). Each of these n-cubes needs to be checked.
for (final String ncube : getReferencedCubeNames())
{
NCube refCube = NCubeManager.getCube(ncube, version);
if (refCube == null)
{
throw new IllegalStateException("Attempting to get required scope, but NCube '" + ncube + "' is not loaded into NCubeManager.");
}
stack.addFirst(refCube);
}
}
// Sort the required scope keys by placing in TreeSet
Set<String> reqScope = new TreeSet<String>(requiredScope);
// Cache required scope for fast retrieval
scopeCache.put(name, reqScope);
// Convert TreeSet to CaseInsensitiveSet which will maintain sorted order, but
// will be case-insensitive on scope keys. Also, the return set is mutable (not
// a reference to the cached required scope).
return new CaseInsensitiveSet<String>(reqScope);
}
}
/**
* @return a Set of Strings, where each String is the name of a scope key (input.variable, where 'variable'
* is a required scope) located within the n-cube cells, inside CommandCells.
*/
private static Set<String> getScopeKeysFromCommandCells(Map<Set<Column>, ?> cubeCells)
{
Set<String> scopeKeys = new CaseInsensitiveSet<String>();
for (Object cell : cubeCells.values())
{
if (cell instanceof CommandCell)
{
CommandCell cmd = (CommandCell) cell;
cmd.getScopeKeys(scopeKeys);
}
}
return scopeKeys;
}
/**
* Find all occurrences of 'input.variable' within conditions on
* a Rule axis. Add 'variable' as required scope key.
* @param ncube NCube to search
* @return Set<String> of required scope (coordinate) keys.
*/
private static Set<String> getScopeKeysFromRuleAxes(NCube<?> ncube)
{
Set<String> scopeKeys = new CaseInsensitiveSet<String>();
for (Axis axis : ncube.getAxes())
{
if (axis.getType() == AxisType.RULE)
{
for (Column column : axis.getColumnsWithoutDefault())
{
CommandCell cmd = (CommandCell) column.getValue();
Matcher m = Regexes.inputVar.matcher(cmd.getCmd());
while (m.find())
{
scopeKeys.add(m.group(2));
}
}
}
}
return scopeKeys;
}
/**
* @return Set<String> names of all referenced cubes within this
* specific NCube. It is not recursive.
*/
Set<String> getReferencedCubeNames()
{
final Set<String> cubeNames = new LinkedHashSet<String>();
for (final Object cell : cells.values())
{
if (cell instanceof CommandCell)
{
final CommandCell cmdCell = (CommandCell) cell;
cmdCell.getCubeNamesFromCommandText(cubeNames);
}
}
for (Axis axis : getAxes())
{
if (axis.getType() == AxisType.RULE)
{
for (Column column : axis.getColumnsWithoutDefault())
{
CommandCell cmd = (CommandCell) column.getValue();
cmd.getCubeNamesFromCommandText(cubeNames);
}
}
}
return cubeNames;
}
/**
* Use this API to generate an HTML view of this NCube.
* @param headers String list of axis names to place at top. If more than one is listed, the first axis encountered that
* matches one of the passed in headers, will be the axis chosen to be displayed at the top.
* @return String containing an HTML view of this NCube.
*/
public String toHtml(String ... headers)
{
return new HtmlFormatter(headers).format(this);
}
public String toFormattedJson()
{
return new JsonFormatter().format(this);
}
public String toString()
{
return toFormattedJson();
}
// ----------------------------
// Overall cube management APIs
// ----------------------------
/**
* @return String in JSON format that contains this entire ncube
*/
public String toJson()
{
try
{
return JsonWriter.objectToJson(this);
}
catch (IOException e)
{
throw new RuntimeException("error writing NCube '" + name + "' in JSON format", e);
}
}
/**
* Given the passed in JSON, return an NCube from it
* @param json String JSON format of an NCube.
*/
public static NCube fromJson(final String json)
{
try
{
return (NCube) JsonReader.jsonToJava(json);
}
catch (Exception e)
{
throw new RuntimeException("Error reading NCube from passed in JSON", e);
}
}
/**
* Use this API to create NCubes from a simple JSON format.
* It is called simple because not all ncubes can be built
* using this format. There is no way to specify a generic
* Comparable column. Other than that, all options are supported,
* including all axisTypes and all axisValueTypes.
*
* If you need support for a cell type that is a Java 'Airport' class
* for example, then use the toJson / fromJson APIs (or create the NCube
* from Java code).
*
* @param json Simple JSON format
* @return NCube instance created from the passed in JSON. It is
* not added to the static list of NCubes. If you want that, call
* addCube() after creating the NCube with this API.
*/
public static NCube<?> fromSimpleJson(final String json)
{
try
{
Map<Object, Long> userIdToUniqueId = new CaseInsensitiveMap<Object, Long>();
Map jsonNCube = JsonReader.jsonToMaps(json);
String cubeName = getString(jsonNCube, "ncube");
if (StringUtilities.isEmpty(cubeName))
{
throw new IllegalArgumentException("JSON format must have a root 'ncube' field containing the String name of the NCube.");
}
NCube ncube = new NCube(cubeName);
ncube.setVersion("file");
ncube.metaProps = new CaseInsensitiveMap();
ncube.metaProps.putAll(jsonNCube);
ncube.metaProps.remove("ncube");
ncube.metaProps.remove("defaultCellValue");
ncube.metaProps.remove("defaultCellValueType");
ncube.metaProps.remove("ruleMode");
ncube.metaProps.remove("axes");
ncube.metaProps.remove("cells");
if (ncube.metaProps.size() < 1)
{ // No additional props, don't even waste space for additional meta properties.
ncube.metaProps = null;
}
String defType = (String) jsonNCube.get("defaultCellValueType");
ncube.defaultCellValue = parseJsonValue(jsonNCube.get("defaultCellValue"), null, defType, false);
ncube.ruleMode = getBoolean(jsonNCube, "ruleMode");
if (!(jsonNCube.get("axes") instanceof JsonObject))
{
throw new IllegalArgumentException("Must specify a list of axes for the ncube, under the key 'axes' as [{axis 1}, {axis 2}, ... {axis n}].");
}
JsonObject axes = (JsonObject) jsonNCube.get("axes");
Object[] items = axes.getArray();
if (ArrayUtilities.isEmpty(items))
{
throw new IllegalArgumentException("Must be at least one axis defined in the JSON format.");
}
// Read axes
for (Object item : items)
{
Map jsonAxis = (Map) item;
String name = getString(jsonAxis, "name");
AxisType type = AxisType.valueOf(getString(jsonAxis, "type"));
boolean hasDefault = getBoolean(jsonAxis, "hasDefault");
AxisValueType valueType = AxisValueType.valueOf(getString(jsonAxis, "valueType"));
final int preferredOrder = getLong(jsonAxis, "preferredOrder").intValue();
final Boolean multiMatch = getBoolean(jsonAxis, "multiMatch");
Axis axis = new Axis(name, type, valueType, hasDefault, preferredOrder, multiMatch);
ncube.addAxis(axis);
axis.metaProps = new CaseInsensitiveMap();
axis.metaProps.putAll(jsonAxis);
axis.metaProps.remove("name");
axis.metaProps.remove("type");
axis.metaProps.remove("hasDefault");
axis.metaProps.remove("valueType");
axis.metaProps.remove("preferredOrder");
axis.metaProps.remove("multiMatch");
axis.metaProps.remove("columns");
if (axis.metaProps.size() < 1)
{
axis.metaProps = null;
}
if (!(jsonAxis.get("columns") instanceof JsonObject))
{
throw new IllegalArgumentException("'columns' must be specified, axis '" + name + "', NCube '" + cubeName + "'");
}
JsonObject colMap = (JsonObject) jsonAxis.get("columns");
if (!colMap.isArray())
{
throw new IllegalArgumentException("'columns' must be an array, axis '" + name + "', NCube '" + cubeName + "'");
}
// Read columns
Object[] cols = colMap.getArray();
for (Object col : cols)
{
Map jsonColumn = (Map) col;
Object value = jsonColumn.get("value");
String url = (String)jsonColumn.get("url");
String colType = (String) jsonColumn.get("type");
Object id = jsonColumn.get("id");
if (value == null)
{
if (id == null)
{
throw new IllegalArgumentException("Missing 'value' field on column or it is null, axis '" + name + "', NCube '" + cubeName + "'");
}
else
{ // Allows you to skip setting both id and value to the same value.
value = id;
}
}
boolean cache = true;
if (jsonColumn.containsKey("cache"))
{
if (jsonColumn.get("cache") instanceof Boolean)
{
cache = (Boolean) jsonColumn.get("cache");
}
else if (jsonColumn.get("cache") instanceof String)
{ // Allow setting it as a String too
cache = "true".equalsIgnoreCase((String)jsonColumn.get("cache"));
}
else
{
throw new IllegalArgumentException("'cache' parameter must be set to 'true' or 'false', or not used (defaults to 'true')");
}
}
Column colAdded;
if (type == AxisType.DISCRETE || type == AxisType.NEAREST)
{
colAdded = ncube.addColumn(axis.getName(), (Comparable) parseJsonValue(value, null, colType, false));
}
else if (type == AxisType.RANGE)
{
Object[] rangeItems = ((JsonObject)value).getArray();
if (rangeItems.length != 2)
{
throw new IllegalArgumentException("Range must have exactly two items, axis '" + name +"', NCube '" + cubeName + "'");
}
Comparable low = (Comparable) parseJsonValue(rangeItems[0], null, colType, false);
Comparable high = (Comparable) parseJsonValue(rangeItems[1], null, colType, false);
colAdded = ncube.addColumn(axis.getName(), new Range(low, high));
}
else if (type == AxisType.SET)
{
Object[] rangeItems = ((JsonObject)value).getArray();
RangeSet rangeSet = new RangeSet();
for (Object pt : rangeItems)
{
if (pt instanceof Object[])
{
Object[] rangeValues = (Object[]) pt;
Comparable low = (Comparable) parseJsonValue(rangeValues[0], null, colType, false);
Comparable high = (Comparable) parseJsonValue(rangeValues[1], null, colType, false);
Range range = new Range(low, high);
rangeSet.add(range);
}
else
{
rangeSet.add((Comparable)parseJsonValue(pt, null, colType, false));
}
}
colAdded = ncube.addColumn(axis.getName(), rangeSet);
}
else if (type == AxisType.RULE)
{
Object cmd = parseJsonValue(value, url, colType, cache);
if (!(cmd instanceof CommandCell))
{
throw new IllegalArgumentException("Column values on a RULE axis must be of type CommandCell, axis '" + name + "', NCube '" + cubeName + "'");
}
colAdded = ncube.addColumn(axis.getName(), (CommandCell)cmd);
}
else
{
throw new IllegalArgumentException("Unsupported Axis Type '" + type + "' for simple JSON input, axis '" + name + "', NCube '" + cubeName + "'");
}
if (id != null)
{
long sysId = colAdded.getId();
userIdToUniqueId.put(id, sysId);
}
colAdded.metaProps = new CaseInsensitiveMap();
colAdded.metaProps.putAll(jsonColumn);
colAdded.metaProps.remove("id");
colAdded.metaProps.remove("value");
colAdded.metaProps.remove("type");
colAdded.metaProps.remove("url");
colAdded.metaProps.remove("cache");
if (colAdded.metaProps.size() < 1)
{
colAdded.metaProps = null;
}
}
}
// Read cells
if (!(jsonNCube.get("cells") instanceof JsonObject))
{
throw new IllegalArgumentException("Must specify the 'cells' portion. It can be empty but must be specified, NCube '" + cubeName + "'");
}
JsonObject cellMap = (JsonObject) jsonNCube.get("cells");
if (!cellMap.isArray())
{
throw new IllegalArgumentException("'cells' must be an []. It can be empty but must be specified, NCube '" + cubeName + "'");
}
Object[] cells = cellMap.getArray();
for (Object cell : cells)
{
JsonObject cMap = (JsonObject) cell;
Object ids = cMap.get("id");
String type = (String) cMap.get("type");
String url = (String) cMap.get("url");
boolean cache = false;
if (cMap.containsKey("cache"))
{
if (cMap.get("cache") instanceof Boolean)
{
cache = (Boolean) cMap.get("cache");
}
else
{
throw new IllegalArgumentException("'cache' parameter must be set to 'true' or 'false', or not used (defaults to 'true')");
}
}
Object v = parseJsonValue(cMap.get("value"), url, type, cache);
if (ids instanceof JsonObject)
{ // If specified as ID array, build coordinate that way
Set<Long> colIds = new HashSet<Long>();
for (Object id : ((JsonObject)ids).getArray())
{
if (!userIdToUniqueId.containsKey(id))
{
throw new IllegalArgumentException("ID specified in cell does not match an ID in the columns, id: " + id);
}
colIds.add(userIdToUniqueId.get(id));
}
ncube.setCellById(v, colIds);
}
else
{ // specified as key-values along each axis
if (!(cMap.get("key") instanceof JsonObject))
{
throw new IllegalArgumentException("'key' must be a JSON object {}, NCube '" + cubeName + "'");
}
JsonObject<String, Object> keys = (JsonObject<String, Object>) cMap.get("key");
for (Map.Entry<String, Object> entry : keys.entrySet())
{
keys.put(entry.getKey(), parseJsonValue(entry.getValue(), null, null, false));
}
ncube.setCell(v, keys);
}
}
return ncube;
}
catch (Exception e)
{
throw new RuntimeException("Error reading NCube from passed in JSON", e);
}
}
private static String getString(Map obj, String key)
{
Object val = obj.get(key);
if (val instanceof String)
{
return (String) val;
}
String clazz = val == null ? "null" : val.getClass().getName();
throw new IllegalArgumentException("Expected 'String' for key '" + key + "' but instead found: " + clazz);
}
private static Long getLong(Map obj, String key)
{
Object val = obj.get(key);
if (val instanceof Long)
{
return (Long) val;
}
String clazz = val == null ? "null" : val.getClass().getName();
throw new IllegalArgumentException("Expected 'Long' for key '" + key + "' but instead found: " + clazz);
}
private static Boolean getBoolean(Map obj, String key)
{
Object val = obj.get(key);
if (val instanceof Boolean)
{
return (Boolean) val;
}
if (val == null)
{
return false;
}
String clazz = val.getClass().getName();
throw new IllegalArgumentException("Expected 'Boolean' for key '" + key + "' but instead found: " + clazz);
}
private static Object parseJsonValue(final Object value, final String url, final String type, boolean cache)
{
if (url != null)
{
if ("exp".equalsIgnoreCase(type))
{
return new GroovyExpression(null, url);
}
else if ("method".equalsIgnoreCase(type))
{
return new GroovyMethod(null, url);
}
else if ("template".equalsIgnoreCase(type))
{
return new GroovyTemplate(null, url, cache);
}
else if ("string".equalsIgnoreCase(type))
{
return new StringUrlCmd(url, cache);
}
else if ("binary".equalsIgnoreCase(type))
{
return new BinaryUrlCmd(url, cache);
}
else
{
throw new IllegalArgumentException("url can only be specified with 'exp', 'method', 'template', 'string', or 'binary' types");
}
}
if ("null".equals(value) || value == null)
{
return null;
}
else if (value instanceof Double)
{
if ("bigdec".equals(type))
{
return new BigDecimal((Double)value);
}
else if ("float".equals(type))
{
return ((Double)value).floatValue();
}
return value;
}
else if (value instanceof Long)
{
if ("int".equals(type))
{
return ((Long)value).intValue();
}
else if ("bigint".equals(type))
{
return new BigInteger(value.toString());
}
else if ("byte".equals(type))
{
return ((Long)value).byteValue();
}
else if ("short".equals(type))
{
return (((Long) value).shortValue());
}
else if ("bigdec".equals(type))
{
return new BigDecimal((Long)value);
}
return value;
}
else if (value instanceof Boolean)
{
return value;
}
else if (value instanceof String)
{
if (StringUtilities.isEmpty(type))
{
return value;
}
else if ("exp".equals(type))
{
return new GroovyExpression((String)value, null);
}
else if ("method".equals(type))
{
return new GroovyMethod((String) value, null);
}
else if ("date".equals(type) || "datetime".equals(type))
{
try
{
Date date = DateUtilities.parseDate((String) value);
return (date == null) ? value : date;
}
catch (Exception e)
{
throw new IllegalArgumentException("Could not parse '" + type + "': " + value);
}
}
else if ("template".equals(type))
{
return new GroovyTemplate((String)value, null, true);
}
else if ("string".equals(type))
{
return value;
}
else if ("binary".equals(type))
{ // convert hex string "10AF3F" as byte[]
return StringUtilities.decode((String) value);
}
else if ("bigint".equals(type))
{
return new BigInteger((String) value);
}
else if ("bigdec".equals(type))
{
return new BigDecimal((String)value);
}
else if ("latlon".equals(type))
{
Matcher m = Regexes.valid2Doubles.matcher((String) value);
if (!m.matches())
{
throw new IllegalArgumentException(String.format("Illegal Lat/Long value (%s)", value));
}
return new LatLon(Double.parseDouble(m.group(1)), Double.parseDouble(m.group(2)));
}
else if ("point2d".equals(type))
{
Matcher m = Regexes.valid2Doubles.matcher((String) value);
if (!m.matches())
{
throw new IllegalArgumentException(String.format("Illegal Point2D value (%s)", value));
}
return new Point2D(Double.parseDouble(m.group(1)), Double.parseDouble(m.group(2)));
}
else if ("point3d".equals(type))
{
Matcher m = Regexes.valid3Doubles.matcher((String) value);
if (!m.matches())
{
throw new IllegalArgumentException(String.format("Illegal Point3D value (%s)", value));
}
return new Point3D(Double.parseDouble(m.group(1)), Double.parseDouble(m.group(2)), Double.parseDouble(m.group(3)));
}
else
{
throw new IllegalArgumentException("Unknown value (" + type + ") for 'type' field");
}
}
else if (value instanceof JsonObject)
{
Object[] values = ((JsonObject) value).getArray();
for (int i=0; i < values.length; i++)
{
values[i] = parseJsonValue(values[i], null, type, false);
}
return values;
}
else
{
throw new IllegalArgumentException("Error reading value of type '" + value.getClass().getName() + "' - Simple JSON format for NCube only supports Long, Double, String, String Date, Boolean, or null");
}
}
/**
* Create an equivalent n-cube as 'this', however, ensure that all IDs are unique
* within the ncube. This means it cannot be created with a traditional 'json-io clone'
* technique.
*/
public NCube duplicate(String newName)
{
NCube copyCube = new NCube(newName);
copyCube.setRuleMode(ruleMode);
copyCube.setDefaultCellValue(defaultCellValue);
Map<Long, Column> origToNewColumn = new HashMap<Long, Column>();
for (Axis axis : axisList.values())
{
Axis copyAxis = new Axis(axis.getName(), axis.getType(), axis.getValueType(), axis.hasDefaultColumn(), axis.getColumnOrder(), axis.isMultiMatch());
for (Column column : axis.getColumns())
{
Column newCol = column.isDefault() ? copyAxis.getDefaultColumn() : copyAxis.addColumn(column.getValue());
origToNewColumn.put(column.id, newCol);
}
copyCube.addAxis(copyAxis);
}
for (Map.Entry<Set<Column>, T> entry : cells.entrySet())
{
Set<Column> copyKey = new HashSet<Column>();
for (Column column : entry.getKey())
{
copyKey.add(origToNewColumn.get(column.id));
}
copyCube.cells.put(copyKey, entry.getValue());
}
return copyCube;
}
public boolean equals(Object other)
{
if (!(other instanceof NCube))
{
return false;
}
NCube that = (NCube) other;
if (!name.equals(that.name))
{
return false;
}
if (ruleMode != that.ruleMode)
{
return false;
}
if (defaultCellValue == null)
{
if (that.defaultCellValue != null)
{
return false;
}
}
else
{
if (!defaultCellValue.equals(that.defaultCellValue))
{
return false;
}
}
if (axisList.size() != that.axisList.size())
{
return false;
}
Map<Column, Column> idMap = new HashMap<Column, Column>();
for (Map.Entry<String, Axis> entry : axisList.entrySet())
{
if (!that.axisList.containsKey(entry.getKey()))
{
return false;
}
Axis thisAxis = entry.getValue();
Axis thatAxis = (Axis) that.axisList.get(entry.getKey());
if (!thisAxis.getName().equalsIgnoreCase(thatAxis.getName()))
{
return false;
}
if (thisAxis.getColumnOrder() != thatAxis.getColumnOrder())
{
return false;
}
if (thisAxis.getType() != thatAxis.getType())
{
return false;
}
if (thisAxis.getValueType() != thatAxis.getValueType())
{
return false;
}
if (thisAxis.getColumns().size() != thatAxis.getColumns().size())
{
return false;
}
if (thisAxis.hasDefaultColumn() != thatAxis.hasDefaultColumn())
{
return false;
}
Iterator<Column> iThisCol = thisAxis.getColumns().iterator();
Iterator<Column> iThatCol = thatAxis.getColumns().iterator();
while (iThisCol.hasNext())
{
Column thisCol = iThisCol.next();
Column thatCol = iThatCol.next();
if (thisCol.getValue() == null)
{
if (thatCol.getValue() != null)
{
return false;
}
}
else if (!thisCol.getValue().equals(thatCol.getValue()))
{
return false;
}
idMap.put(thisCol, thatCol);
}
}
if (cells.size() != that.cells.size())
{
return false;
}
for (Map.Entry<Set<Column>, T> entry : cells.entrySet())
{
Set<Column> cellKey = entry.getKey();
T value = entry.getValue();
Set<Column> thatCellKey = new HashSet<Column>();
for (Column column : cellKey)
{
thatCellKey.add(idMap.get(column));
}
Object thatCellValue = that.cells.get(thatCellKey);
if (!DeepEquals.deepEquals(value, thatCellValue))
{
return false;
}
}
return true;
}
public int hashCode()
{
StringBuilder s = new StringBuilder(name);
if (defaultCellValue != null)
{
s.append(defaultCellValue.toString());
}
s.append(ruleMode ? '1' : '0');
for (Axis axis : axisList.values())
{
s.append(axis.getName().toLowerCase());
s.append(axis.getColumnOrder());
s.append(axis.getType());
s.append(axis.getValueType());
for (Column column : axis.getColumnsWithoutDefault())
{
s.append(column.getValue());
}
s.append(axis.hasDefaultColumn() ? '1' : '0');
}
int h1 = EncryptionUtilities.calculateSHA1Hash(s.toString().getBytes()).hashCode();
s.setLength(0);
int h2 = 0;
for (Map.Entry<Set<Column>, T> entry : cells.entrySet())
{
Set<Column> cellKey = entry.getKey();
T value = entry.getValue();
for (Column column : cellKey)
{
if (column.getValue() != null)
{
h2 += column.getValue().hashCode();
}
}
if (value != null)
{
h2 += DeepEquals.deepHashCode(value);
}
}
return h1 + h2;
}
}