Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
package com.cedarsoftware.ncube
import com.cedarsoftware.ncube.exception.CommandCellException
import com.cedarsoftware.ncube.exception.CoordinateNotFoundException
import com.cedarsoftware.ncube.exception.InvalidCoordinateException
import com.cedarsoftware.ncube.exception.RuleJump
import com.cedarsoftware.ncube.exception.RuleStop
import com.cedarsoftware.ncube.formatters.HtmlFormatter
import com.cedarsoftware.ncube.formatters.JsonFormatter
import com.cedarsoftware.ncube.formatters.NCubeTestReader
import com.cedarsoftware.ncube.formatters.NCubeTestWriter
import com.cedarsoftware.ncube.util.CellMap
import com.cedarsoftware.util.AdjustableGZIPOutputStream
import com.cedarsoftware.util.ByteUtilities
import com.cedarsoftware.util.CaseInsensitiveMap
import com.cedarsoftware.util.CaseInsensitiveSet
import com.cedarsoftware.util.CompactCIHashMap
import com.cedarsoftware.util.CompactCILinkedMap
import com.cedarsoftware.util.CompactMap
import com.cedarsoftware.util.LongHashSet
import com.cedarsoftware.util.MapUtilities
import com.cedarsoftware.util.MathUtil
import com.cedarsoftware.util.TrackingMap
import com.cedarsoftware.util.io.JsonObject
import com.cedarsoftware.util.io.JsonWriter
import com.fasterxml.jackson.core.JsonFactory
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import org.springframework.util.FastByteArrayOutputStream
import java.lang.reflect.Array
import java.lang.reflect.Field
import java.security.MessageDigest
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
import java.util.regex.Matcher
import java.util.regex.Pattern
import java.util.zip.Deflater
import java.util.zip.GZIPInputStream
import static com.cedarsoftware.ncube.NCubeAppContext.trackBindingsOn
import static com.cedarsoftware.ncube.NCubeConstants.DECISION_TABLE
import static com.cedarsoftware.ncube.NCubeConstants.DONT_TRACK_INPUT_KEYS_USED
import static com.cedarsoftware.ncube.NCubeConstants.NO_STACKFRAME
import static com.cedarsoftware.util.Converter.convertToInteger
import static com.cedarsoftware.util.Converter.convertToLong
import static com.cedarsoftware.util.EncryptionUtilities.SHA1Digest
import static com.cedarsoftware.util.EncryptionUtilities.calculateSHA1Hash
import static com.cedarsoftware.util.ExceptionUtilities.getDeepestException
import static com.cedarsoftware.util.IOUtilities.close
import static com.cedarsoftware.util.ReflectionUtils.getDeepDeclaredFields
import static com.cedarsoftware.util.StringUtilities.encode
import static com.cedarsoftware.util.StringUtilities.hasContent
import static com.cedarsoftware.util.StringUtilities.isEmpty
import static com.cedarsoftware.util.StringUtilities.wildcardToRegexString
import static com.cedarsoftware.util.io.MetaUtils.isLogicalPrimitive
/**
* 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.
*
* Useful for pricing, rating, and configuration modeling.
*
* @author John DeRegnaucourt ([email protected])
*
* Copyright (c) Cedar Software LLC
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@Slf4j
@CompileStatic
class NCube
{
public static final String DEFAULT_CELL_VALUE_TYPE = 'defaultCellValueType'
public static final String DEFAULT_CELL_VALUE = 'defaultCellValue'
public static final String DEFAULT_CELL_VALUE_URL = 'defaultCellValueUrl'
public static final String DEFAULT_CELL_VALUE_CACHE = 'defaultCellValueCache'
public static final String validCubeNameChars = '0-9a-zA-Z._-'
public static final String RULE_EXEC_INFO = '_rule'
public static final String METAPROPERTY_TEST_UPDATED = 'testUpdated'
public static final String METAPROPERTY_TEST_DATA = '_testData'
public static final String MAP_REDUCE_COLUMNS_TO_SEARCH = 'columnsToSearch'
public static final String MAP_REDUCE_COLUMNS_TO_RETURN = 'columnsToReturn'
public static final String MAP_REDUCE_SHOULD_EXECUTE = 'shouldExecute'
public static final String MAP_REDUCE_DEFAULT_VALUE = 'defaultValue'
protected static final byte[] TRUE_BYTES = 't'.bytes
protected static final byte[] FALSE_BYTES = 'f'.bytes
private static final byte[] A_BYTES = 'a'.bytes
private static final byte[] C_BYTES = 'c'.bytes
private static final byte[] O_BYTES = 'o'.bytes
private static final byte[] NULL_BYTES = 'null'.bytes
private static final byte[] ARRAY_BYTES = 'array'.bytes
private static final byte[] MAP_BYTES = 'map'.bytes
private static final byte[] COL_BYTES = 'col'.bytes
private String name
private String sha1
private final Map axisList = new CompactCILinkedMap<>()
private final Map idToAxis = new Long2ObjectOpenHashMap<>()
protected final Map, T> cells = new CellMap()
private T defaultCellValue
private final Map advices = new CompactMap<>()
private Map metaProps = new CompactCILinkedMap<>()
private static ConcurrentMap primitives = new ConcurrentHashMap()
// Sets up the defaultApplicationId for cubes loaded in from disk.
private transient ApplicationID appId = ApplicationID.testAppId
private transient Object engine = null
private static final ThreadLocal> executionStack = new ThreadLocal>() {
Deque initialValue()
{
return new ArrayDeque<>()
}
}
private static int stackEntryCoordinateValueMaxSize
private static List stackEntryInputKeyExclusions
/**
* Creata a new NCube instance with the passed in name
* @param name String name to use for the NCube.
*/
NCube(String name)
{
if (name != null)
{ // If name is null, likely being instantiated via serialization
validateCubeName(name)
}
this.name = name
}
/**
* @return reference to associated business engine (RulesEngine, DecisionTable, TruthTable, ...)
*/
Object getBusinessEngine()
{
return engine
}
/**
* Associate business engine (RulesEngine, DecisionTable, TruthTable, ...) to this NCube.
* @param businessEngine Object engine to associate.
*/
void setBusinessEngine(Object businessEngine)
{
engine = businessEngine
}
/**
* Fetch n-cube meta properties (SHA1, HEAD_SHA1, and CHANGE_TYPE are not meta-properties.
* Use their respective accessor functions to obtain those).
* @return Map (case insensitive keys) containing meta (additional) properties for the n-cube.
* Modifications to this Map do not modify the actual meta properties of n-cube. To do that,
* you need to use setMetaProperty(), addMetaProperty(), or remoteMetaProperty()
*/
Map getMetaProperties()
{
return Collections.unmodifiableMap(metaProps)
}
/**
* Fetch the value associated to the passed in Key from the MetaProperties (if any exist). If
* none exist, null is returned.
*/
Object getMetaProperty(String key)
{
return metaProps.get(key)
}
/**
* Test for existence of a given meta-property key.
* @param key String name of key
* @return boolean true if the passed in meta-property key exists, false otherwise.
*/
boolean containsMetaProperty(String key)
{
return metaProps.containsKey(key)
}
/**
* If a meta property value is fetched from an Axis or a Column, the value should be extracted
* using this API, so as to allow executable values to be retrieved.
* @param value Object value to be extracted.
*/
Object extractMetaPropertyValue(Object value, Map input = [:], Map output = [:])
{
if (value instanceof CommandCell)
{
if (!(input instanceof TrackingMap))
{
input = new TrackingMap(input)
}
CommandCell cmd = (CommandCell) value
value = executeExpression(prepareExecutionContext(input, output), cmd)
}
return value
}
/**
* 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
*/
Object setMetaProperty(String key, Object value)
{
clearSha1()
return metaProps.put(key, value)
}
/**
* Remove a meta-property entry
*/
Object removeMetaProperty(String key)
{
Object prop = metaProps.remove(key)
clearSha1()
return prop
}
/**
* Add a Map of meta properties all at once.
* @param allAtOnce Map of meta properties to add
*/
void addMetaProperties(Map allAtOnce)
{
for (entry in allAtOnce.entrySet())
{
final String key = entry.key
metaProps.put(key, entry.value)
}
clearSha1()
}
/**
* Remove all meta properties associated to this n-cube.
*/
void clearMetaProperties()
{
metaProps.clear()
clearSha1()
}
/**
* Walk cell map and ensure all coordinates are fully resolvable
*/
protected void dropOrphans(Set columnIds, long axisId)
{
Iterator> i = cells.keySet().iterator()
while (i.hasNext())
{
Set cols = i.next()
for (id in cols)
{
Axis axis = getAxisFromColumnId(id, false)
if (axis && axis.id == axisId)
{
if (!columnIds.contains(id))
{
i.remove()
break
}
}
}
}
}
/**
* 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).
*/
private static class StackEntry
{
final String cubeName
final Map coord
StackEntry(String name, Map coordinate)
{
cubeName = name
coord = coordinate
}
String toString()
{
StringBuilder s = new StringBuilder()
s.append("${cubeName}:[")
Iterator i = coord.entrySet().iterator()
int count=0
while (i.hasNext())
{
Map.Entry coordinate = i.next()
String key = coordinate.key
if (stackEntryInputKeyExclusions!=null && shouldExcludeKey(key)) {
continue
}
String value = coordinate.value.toString()
if (value.size() > stackEntryCoordinateValueMaxSize)
{
value = "${value[0..(stackEntryCoordinateValueMaxSize - 1)]}..."
}
if (count++>0) {
s.append(',')
}
s.append("${key}:${value}")
}
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
*/
protected void addAdvice(Advice advice, String method)
{
advices.put("${advice.name}/${method}".toString(), advice)
}
/**
* @return List advices added to this n-cube.
*/
List getAdvices(String method)
{
if (advices.isEmpty())
{
return Collections.emptyList()
}
List result = new ArrayList<>()
method = "/${method}"
for (entry in advices.entrySet())
{
// Entry key = "AdviceName/MethodName"
if (entry.key.endsWith(method))
{ // Entry.Value = Advice instance
result.add(entry.value)
}
}
if (!result.empty)
{
Collections.sort(result, new Comparator() {
int compare(Advice a1, Advice a2)
{
return a1.name.compareToIgnoreCase(a2.name)
}
})
}
return result
}
/**
* For testing, advices need to be removed after test completes.
*/
protected void clearAdvices()
{
advices.clear()
}
/**
* @return ReleaseStatus of this n-cube as it was loaded.
*/
String getStatus()
{
return appId.status
}
/**
* @return String version of this n-cube as it was loaded.
*/
String getVersion()
{
return appId.version
}
/**
* @return ApplicationID for this n-cube. This contains the app name, version, etc. that this
* n-cube is part of.
*/
ApplicationID getApplicationID()
{
return appId
}
void setApplicationID(ApplicationID appId)
{
this.appId = appId
}
/**
* This should only be called from NCubeManager when loading the cube from a database
* It is mainly to prevent an unnecessary sha1 calculation after being loaded from a
* db that already knows the sha1.
* @param sha1 String SHA-1 value to set into this n-cube. Should only be called internally
* from code constructing this n-cube from a persistent store.
*/
protected void setSha1(String sha1)
{
this.sha1 = sha1
}
/**
* @return String name of the NCube
*/
String getName()
{
return name
}
/**
* Clear (remove) the cell at the given coordinate. The cell is dropped
* from the internal sparse storage. After this call, containsCell(coord) for the
* same cell will return false.
* @param coordinate Map coordinate of Cell to remove.
* @return value of cell that was removed.
* For RULE axes, the name of the Rule Axis must be bound to a rule name (e.g. the 'name'
* attribute on the Column expression). If you need to distinguish between a null
* stored in a null, versus nothing being stored there, use containsCell() first.
*/
T removeCell(final Map coordinate)
{
clearSha1()
return cells.remove(getCoordinateKey(coordinate))
}
/**
* Clear a cell directly from the cell sparse-matrix specified by the passed in Column
* IDs. After this call, containsCell(coord) for the same coordinate would return false.
*/
T removeCellById(final Set coordinate)
{
clearSha1()
Set ids = ensureFullCoordinate(coordinate)
if (ids == null)
{
return null
}
return cells.remove(ids)
}
/**
* Test to see if a value is mapped at the given coordinate. The 2nd argument allows
* you to take into account the n-cube level Default Cell value or not. Set to true
* to have the default value considered, false otherwise.
* @param coordinate Map (coordinate) of a cell
* @param useDefault (optional, defaults to false. Set to true, then if a non-default
* value for the n-cube is set, this method will return true).
* @return 1. boolean true if a defaultValue is set (non-null) and useDefault is true
* 2. boolean true if a cell is located at the specified coordinate in the
* sparse cell map.
* For RULE axes, the name of the Rule Axis must be bound to a rule name
* (e.g. the 'name' attribute on the Column expression).
*/
boolean containsCell(final Map coordinate, boolean useDefault = false)
{
Set cols
if (useDefault)
{
if (defaultCellValue != null)
{ // n-cube default not-null, so yes it 'contains cell' (when useDefault true)
return true
}
cols = getCoordinateKey(coordinate)
if (getColumnDefault(cols) != null)
{ // n-cube column default not-null, so yes it 'contains cell' (when useDefault true)
return true
}
}
else
{
cols = getCoordinateKey(coordinate)
}
return cells.containsKey(cols)
}
/**
* @return true if and only if there is a cell stored at the location
* specified by the Set coordinate. If the IDs don't locate a coordinate,
* no exception is thrown - simply false is returned.
* If no coordinate is supplied for an axis (or axes) that has a default column, then the default
* column will be bound to for that axis (or axes).
*/
boolean containsCellById(final Set coordinate)
{
Set ids = ensureFullCoordinate(coordinate)
return cells.containsKey(ids)
}
/**
* 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 nearest column on the axis.
* @return the prior cells value.
*/
T setCell(final T value, final Map coordinate)
{
if (!(value instanceof byte[]) && value != null && value.class.array)
{
throw new IllegalArgumentException("Cannot set a cell to be an array type directly (except byte[]). Instead use GroovyExpression.")
}
clearSha1()
return cells.put(getCoordinateKey(coordinate), (T) internValue(value))
}
/**
* Set a cell directly into the cell sparse-matrix specified by the passed in
* Column IDs.
*/
T setCellById(final T value, final Set coordinate, boolean skipEnsureCheck = false)
{
if (!(value instanceof byte[]) && value != null && value.class.array)
{
throw new IllegalArgumentException("Cannot set a cell to be an array type directly (except byte[]). Instead use GroovyExpression.")
}
clearSha1()
if (skipEnsureCheck)
{
return cells.put(coordinate, (T)internValue(value))
}
Set ids = ensureFullCoordinate(coordinate)
if (ids == null)
{
throw new InvalidCoordinateException("Unable to setCellById() into n-cube: ${name}, appId: ${appId} using coordinate: ${coordinate}. Add column(s) before assigning cells.", name)
}
return cells.put(ids, (T)internValue(value))
}
/**
* 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.
*/
def getCellByIdNoExecute(final Set coordinate)
{
Set ids = ensureFullCoordinate(coordinate)
return cells.get(ids)
}
/**
* Fetch the actual 'formula' at the given cell. In the case of primitives,
* the primitive will be returned. However, in the case of an Expression,
* the Expression will be returned, not executed.
* @return value stored at the given location. Since this method will return
* null when there is no value in an empty (unset) cell, use containsCell() if
* you need to distinguish between not present and null.
* @throws CoordinateNotFoundException if the coordinate does not represent a
* coordinate with the space of this n-cube.
*/
def getCellNoExecute(final Map coordinate)
{
Set ids = getCoordinateKey(coordinate)
return cells.get(ids)
}
/**
* Fetch the contents of the cell at the location specified by the coordinate argument.
* Be aware that if you have any rule cubes in the execution path, they can execute
* more than one cell. The cell value returned is the value of the last cell executed.
* Typically, in a rule cube, you are writing to specific keys within the rule cube, and
* the calling code then accesses the 'output' Map to fetch the values at these specific
* keys.
* @param coordinate Map of String keys to values meant to bind to each axis of the n-cube.
* @param output Map that can be written to by the code within the the n-cubes (for example,
* GroovyExpressions.
* @param defaultValue Object placed here will be returned if there is no cell at the location
* pinpointed by the input coordinate. Normally, the defaulValue of the
* n-cube is returned, but if this parameter is passed a non-null value,
* then it will be returned.
* @return Cell pinpointed by the input coordinate. If there is nothing stored at this
* location, then if there is an axis containing a column with a default value (set as
* meta-property Column.DEFAULT_VALUE [key: 'default_value']), then that will be returned.
* If there is no column with a default value, then the n-cube's default value will be
* returned. If defaultValue is null, then then n-cube defaultValue argument will be returned.
*/
T at(final Map coordinate, final Map output = [:], Object defaultValue = null)
{
return getCell(coordinate, output, defaultValue)
}
/**
* Grab the cell located at altInput, then run it in terms of the input.
*/
T use(Map altInput, Map input, Map output, def defaultCellValue)
{
Map safeCoord = wrapCoordinate(validateCoordinate(altInput, output))
T value = getCellById(getCoordinateKey(safeCoord, output), input, output, defaultCellValue)
RuleInfo info = getRuleInfo(output)
info.setLastExecutedStatement(value)
output.return = value
return value
}
/**
*
Fetch the contents of the cell at the location specified by the coordinate argument.
*
* Note that if you have any rule axes, they can execute more than one time. The value
* returned by this method is the value of the last cell executed. Typically, in a rule cube,
* you are writing to specific keys within the output Map, and the calling code then accesses
* the 'output' Map to fetch the values at these specific keys.
*
* A rule axis name can have a String, Collection, Map, or nothing associated to it.
* - If the value is a String, then it is the name of the rule to begin execution
* (skips past rules ahead of it).
* - If the value associated to a rule axis name is a Collection, then it is considered a
* Collection of rule names to run (orchestration). In that case, only the named rules
* will be executed (their conditions evaluated, and if true, the associated statements).
* - If the associated value is a Map, then the keys will be the names of meta-property keys
* associated to the rule condition column meta-property keys, and the values must match
* the value associated to the meta-property. The special value NCUBE.DONT_CARE can be
* associated to the key, in which case only the key name of the meta-property must match
* the key name in the passed in map in order for the rule to be selected. If there
* is more than one entry in the passed in Map, then the rules must match on all entries.
* This is a conjunction (or AND) - the rules match all keys are selected.
* - If nothing is associated to the rule axis name (or null), then all rules are selected.
*
* Once the rules are selected, all rule conditions are executed. If the condition is true,
* then the associated statement is executed.
*
* Note: More than one rule axis can be added to an n-cube. In this case, each rule axis
* name can have its own orchestration (rule list) to select the rules on the given axis.
*
* @param coordinate Map of String keys to values meant to bind to each axis of the n-cube.
* @param output Map that can be written to by the code within the the n-cubes (for example GroovyExpressions.)
* @param defaultValue Object placed here will be returned if there is no cell at the location
* pinpointed by the input coordinate. Normally, the defaulValue of the
* n-cube is returned, but if this parameter is passed a non-null value,
* then it will be returned.
* @return Cell pinpointed by the input coordinate. If there is nothing stored at this
* location, then if there is an axis containing a column with a default value (set as
* meta-property Column.DEFAULT_VALUE [key: 'default_value']), then that will be returned.
* If there is no column with a default value, then the n-cube's default value will be
* returned. If defaultValue is null, then then n-cube defaultValue argument will be returned.
*/
T getCell(final Map coordinate, final Map output = [:], Object defaultValue = null)
{
Map input = wrapCoordinate(validateCoordinate(coordinate, output))
if (hasRuleAxis())
{
return runRules(coordinate, input, output, defaultValue)
}
else
{ // Perform fast bind and execute.
T lastStatementValue = getCellById(getCoordinateKey(input, output), input, output, defaultValue)
final RuleInfo ruleInfo = getRuleInfo(output)
ruleInfo.setLastExecutedStatement(lastStatementValue)
return output.return = lastStatementValue
}
}
private T runRules(final Map coordinate, Map input, final Map output = [:], Object defaultValue = null)
{
final RuleInfo ruleInfo = getRuleInfo(output)
final boolean trackBindings = isTrackBindingsOn()
boolean run = true
final List bindings = ruleInfo.getAxisBindings()
final int depth = executionStack.get().size()
final int dimensions = numDimensions
final String[] axisNames = axisList.keySet().toArray(new String[dimensions])
Map ctx = prepareExecutionContext(input, output)
T lastStatementValue = null
while (run)
{
run = false
final Map> selectedColumns = selectColumns(input, output) // get [potential subset of] rule columns to execute, per Axis
final Map counters = getCountersPerAxis(axisNames)
final Map cachedConditionValues = [:]
final Map conditionsFiredCountPerAxis = [:]
try
{
while (true)
{
final Binding binding = new Binding(name, depth)
for (axis in axisList.values())
{
final String axisName = axis.name
final Column boundColumn = selectedColumns.get(axisName).get(counters[axisName] - 1)
if (axis.type == AxisType.RULE)
{ // Place value bound to rule axis name on input (that was bound to the input[axisName]).
// into a Wrapper class that acts like a String for rebinding to the rule axis, for nested
// use() calls, but contains a pointer to the original value (and delegates calls to it).
input.put(axisName, new GStringWrapper(boundColumn.columnName, input.get(axisName)))
Object conditionValue
if (!cachedConditionValues.containsKey(boundColumn.id))
{ // Has the condition on the Rule axis been run this execution? If not, run it and cache it.
CommandCell cmd = (CommandCell) boundColumn.value
// If the cmd == null, then we are looking at a default column on a rule axis.
// the conditionValue becomes 'true' for Default column when ruleAxisBindCount = 0
final Integer count = conditionsFiredCountPerAxis.get(axisName)
conditionValue = cmd == null ? isZero(count) : executeExpression(ctx, cmd)
cachedConditionValues.put(boundColumn.id, conditionValue as boolean)
if (conditionValue)
{ // Rule fired
conditionsFiredCountPerAxis.put(axisName, count == null ? 1 : count + 1)
if (!axis.fireAll)
{ // Only fire one condition on this axis (fireAll is false)
counters.put(axisName, 1)
selectedColumns.put(axisName, [boundColumn])
}
if (cmd == null)
{
trackUnboundAxis(output, name, axisName, coordinate.get(axisName))
}
}
}
else
{ // re-use condition on this rule axis (happens when more than one rule axis on an n-cube)
conditionValue = cachedConditionValues.get(boundColumn.id)
}
// 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 (conditionValue)
{
binding.bind(axisName, boundColumn)
}
else
{ // Incomplete binding - no need to attempt further bindings on other axes.
break
}
}
else
{
binding.bind(axisName, boundColumn)
}
}
// Step #2 Execute cell and store return value, associating it to the Axes and Columns it bound to
if (binding.numBoundAxes == dimensions)
{ // Conditions on rule axes that do not evaluate to true, do not generate complete coordinates (intentionally skipped)
if (trackBindings)
{ // Add bindings to output map when running ncube.track.bindings = true (default)
bindings.add(binding)
}
lastStatementValue = executeAssociatedStatement(input, output, ruleInfo, binding)
}
else
{
restoreWrappedValues(input)
}
// Step #3 increment counters (variable radix increment)
if (!incrementVariableRadixCount(counters, selectedColumns, axisNames))
{
break
}
}
// Verify all rule axes were bound 1 or more times
ensureAllRuleAxesBound(coordinate, conditionsFiredCountPerAxis)
}
catch (RuleStop ignored)
{ // ends this execution cycle
ruleInfo.ruleStopThrown()
}
catch (RuleJump e)
{
input = e.coord
run = true
}
}
ruleInfo.setLastExecutedStatement(lastStatementValue)
output.return = lastStatementValue
return lastStatementValue
}
private Object executeExpression(Map ctx, CommandCell cmd)
{
try
{
Object ret = cmd.execute(ctx)
trackInputKeysUsed((Map) ctx.input, (Map) ctx.output)
return ret
}
catch (ThreadDeath | RuleStop | RuleJump e)
{
throw e
}
catch (CoordinateNotFoundException e)
{
String msg = e.message
if (!msg.contains('-> cell:'))
{
throw new CoordinateNotFoundException("${e.message}\nerror occurred in cube: ${name}\n${stackToString()}",
e.cubeName, e.coordinate, e.axisName, e.value)
}
else
{
throw e
}
}
catch (CommandCellException e)
{ // Throw the inner-most CommandCellException to the top
throw e
}
catch (StackOverflowError e)
{
throw new CommandCellException("StackOverflow occurred in cube: ${name} for expression: ${((GroovyBase)cmd)?.getRunnableCode()?.name}\n${stackToString()}", e);
}
catch (Throwable t)
{
throw new CommandCellException("Error occurred in cube: ${name}\n${stackToString()}", t)
}
}
private T executeAssociatedStatement(Map input, Map output, RuleInfo ruleInfo, Binding binding)
{
try
{
final Set colIds = binding.idCoordinate
T statementValue = getCellById(colIds, input, output)
binding.value = statementValue
return statementValue
}
catch (RuleStop e)
{ // Statement threw at RuleStop
binding.value = '[RuleStop]'
// Mark that RULE_STOP occurred
ruleInfo.ruleStopThrown()
throw e
}
catch(RuleJump e)
{ // Statement threw at RuleJump
binding.value = '[RuleJump]'
throw e
}
catch (Exception e)
{
Throwable t = e
while (t.cause != null)
{
t = t.cause
}
String msg = t.message
if (isEmpty(msg))
{
msg = t.class.name
}
binding.value = "[${msg}]".toString()
throw e
}
finally
{
// Replace value bound to rule axis name on input (that was bound to the input[axisName]). The replaced
// value acts like a String for rebinding to the rule axis (nested use() calls), but contains a pointer
// to the original value (and delegates calls to it).
restoreWrappedValues(input)
}
}
/**
* Restore temporarily bound rule condition name (that was bound to the input[axisName]). The rule name
* is bound right before executing the associated statement, so that a call back into the rule cube will
* start on the same rule. After execution, the rule name's original associated value is restored.
* @param input Map typical coordinate input
*/
private void restoreWrappedValues(Map input)
{
for (Map.Entry entry: input.entrySet())
{
if (entry.value instanceof GStringWrapper)
{ // Replace any value that was wrapped with the original value.
def value = ((GStringWrapper) entry.value).originalValue
entry.value = value
}
}
}
/**
* @return boolean true if there is at least one rule axis, false if there are no rule axes.
*/
boolean hasRuleAxis()
{
for (axis in axisList.values())
{
if (AxisType.RULE == axis.type)
{
return true
}
}
return false
}
/**
* Verify that at least one rule on each rule axis fired. If not, then you have a
* CoordinateNotFoundException.
* @param coordinate Input (Map) coordinate for getCell()
* @param conditionsFiredCountPerAxis Map that tracks AxisName to number of fired-columns bound to axis
*/
private void ensureAllRuleAxesBound(Map coordinate, Map conditionsFiredCountPerAxis)
{
for (axis in axisList.values())
{
if (AxisType.RULE == axis.type)
{
String axisName = axis.name
Integer count = conditionsFiredCountPerAxis[axisName]
if (count == null || count < 1)
{
throw new CoordinateNotFoundException("No conditions on the rule axis: ${axisName} fired, and there is no default column on this axis, cube: ${name}, input: ${coordinate}",
name, coordinate, axisName)
}
}
}
}
/**
* The lowest level cell fetch. This method uses the Set 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, then a default value from NCube is returned, if one
* is set. Default value ordering - first, a column level default is used if
* one exists (under Column's meta-key: 'DEFAULT_CELL'). If no column-level
* default is specified (no non-null value provided), then the NCube level default
* is chosen (if it exists). If no NCube level default is specified, then the
* defaultValue passed in is used, if it is non-null. The default value cache
* should only be used with mapReduce because of its repeated calculation of each
* column on all axes.
* 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 (entry not needed for axes with
* default column or rule axes).
*/
T getCellById(final Set colIds, final Map coordinate, final Map output, Object defaultValue = null, boolean shouldExecute = true, Map options = null)
{
boolean pushed = false
Deque stackFrame = null
try
{
if (!options?.containsKey(NO_STACKFRAME))
{
stackFrame = (Deque) executionStack.get()
// Form fully qualified cell lookup (NCube name + coordinate)
// Add fully qualified coordinate to ThreadLocal execution stack
final StackEntry entry = new StackEntry(name, coordinate)
stackFrame.addFirst(entry)
pushed = true
}
// Handy trick for debugging a failed binding (like space after an input)
// if (coordinate.containsKey("debug"))
// { // Dump out all kinds of binding info
// log.info("*** DEBUG getCellById() ***")
// log.info("Axes:")
// for (Axis axis : axisList.values())
// {
// log.info(" axis name: " + axis.name)
// log.info(" axis ID: " + axis.getId())
// log.info(" axis type: " + axis.getType())
// log.info(" axis valueType: " + axis.getValueType())
// log.info(" Columns:")
// for (Column column : axis.getColumns())
// {
// if (hasContent(column.getColumnName()))
// {
// log.info(" column name: " + column.getColumnName())
// }
// log.info(" column value: " + column.value)
// log.info(" column id: " + column.getId())
// }
// }
// log.info("Cells:")
// log.info(" " + cells)
// log.info("Input:")
// log.info(" coord IDs: " + idCoord)
// log.info(" coord Map: " + coordinate)
// }
T cellValue = cells.get(colIds)
if (cellValue == null && !cells.containsKey(colIds))
{ // No cell, look for default
cellValue = (T) getColumnDefault(colIds)
if (cellValue == null)
{ // No Column Default, try NCube default, and finally passed in default
cellValue = defaultCellValue == null ? (T) defaultValue : defaultCellValue
}
}
if (cellValue instanceof CommandCell)
{
if (shouldExecute)
{
Map ctx = prepareExecutionContext(coordinate, output)
return (T) executeExpression(ctx, (CommandCell)cellValue)
}
else
{
return cellValue
}
}
else
{
if (!options?.containsKey(DONT_TRACK_INPUT_KEYS_USED))
{
trackInputKeysUsed(coordinate, output)
}
}
return cellValue
}
finally
{ // Unwind stack: always remove if stacked pushed, even if Exception has been thrown
if (pushed)
{
stackFrame?.removeFirst()
}
}
}
/**
* Pre-compile command cells, meta-properties, and rule conditions that are expressions
*/
CompileInfo compile()
{
CompileInfo compileInfo = new CompileInfo()
compileInfo.setCubeName(this.name)
cells.each { ids, cell ->
if (cell instanceof GroovyBase) {
compileCell(getCoordinateFromIds(ids), (GroovyBase)cell, compileInfo)
}
}
metaProps.each { key, value ->
if (value instanceof GroovyBase) {
compileCell([metaProp:key], (GroovyBase)value, compileInfo)
}
}
axisList.each { axisName, axis ->
axis.columns.each { column ->
if (column.value instanceof GroovyBase) {
compileCell([axis:axisName,column:column.columnName], (GroovyBase)column.value, compileInfo)
}
if (column.metaProps) {
column.metaProps.each { key, value ->
if (value instanceof GroovyBase) {
compileCell([axis:axisName,column:column.columnName,metaProp:key], (GroovyBase)value, compileInfo)
}
}
}
}
if (axis.metaProps) {
axis.metaProps.each { key, value ->
if (value instanceof GroovyBase) {
compileCell([axis:axisName,metaProp:key], (GroovyBase)value, compileInfo)
}
}
}
}
return compileInfo
}
private void compileCell(Map input, GroovyBase groovyBase, CompileInfo compileInfo) {
try
{
groovyBase.prepare(groovyBase.cmd ?: groovyBase.url, prepareExecutionContext(input,[:]))
}
catch (Exception e)
{
compileInfo.addException(input,e)
log.warn("Failed to compile cell for cube: ${name} with coords: ${input.toString()}", e)
}
}
/**
* Given the passed in column IDs, return the column level default value
* if one exists or null otherwise. In the case of intersection, then null
* is returned, meaning that the n-cube level default cell value will be
* returned at intersections. The default value cache should only be used
* with mapReduce because of its repeated calculation of each column on all axes.
*/
def getColumnDefault(Set colIds)
{
def colDef = null
Iterator i = colIds.iterator()
while (i.hasNext())
{
long colId = i.next()
Axis axis = getAxisFromColumnId(colId, false)
if (axis == null)
{ // bad column id, continue check rest of column ids
continue
}
Column boundCol = axis.getColumnById(colId)
Object defColValue
if (boundCol != null)
{
defColValue = boundCol.getMetaProperty(Column.DEFAULT_VALUE)
}
if (defColValue != null)
{
if (colDef != null && colDef != defColValue)
{ // More than one specified in this set (intersection), therefore return null (use n-cube level default)
return null
}
colDef = defColValue
}
}
return colDef
}
private static void trackInputKeysUsed(Map input, Map output)
{
if (input instanceof TrackingMap)
{
RuleInfo ruleInfo = getRuleInfo(output)
ruleInfo.addInputKeysUsed(((TrackingMap)input).keysUsed())
}
}
/**
* Prepare the execution context by providing it with references to
* important items like the input coordinate, output map, stack,
* and this (ncube).
*/
protected Map prepareExecutionContext(final Map coord, final Map output)
{
return [input: coord, output: output, ncube: this] // Input coordinate is already a duplicate at this point
}
/**
* 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).
* @param output Map that can be written to by the code within the the n-cubes (for example,
* GroovyExpressions. Optional.
* @param defaultValue Object placed here will be returned if there is no cell at the location
* pinpointed by the input coordinate. Normally, the defaulValue of the
* n-cube is returned, but if this parameter is passed a non-null value,
* then it will be returned. Optional.
* @return a Map containing Axis names and values to bind to those axes. One of the
* axes must have a Set bound to it.
*/
Map