Package com.cedarsoftware.ncube

Source Code of com.cedarsoftware.ncube.NCube$StackEntry

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.util.ArrayUtilities;
import com.cedarsoftware.util.CaseInsensitiveMap;
import com.cedarsoftware.util.CaseInsensitiveSet;
import com.cedarsoftware.util.EncryptionUtilities;
import com.cedarsoftware.util.ReflectionUtils;
import com.cedarsoftware.util.SafeSimpleDateFormat;
import com.cedarsoftware.util.StringUtilities;
import com.cedarsoftware.util.SystemUtilities;
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.Array;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.ParseException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
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;
import java.util.regex.Pattern;

/**
* 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
* manage a list of NCubes.
*
* @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;
    private static SafeSimpleDateFormat datetimeFormat = new SafeSimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
    private static SafeSimpleDateFormat dateFormat = new SafeSimpleDateFormat("yyyy-MM-dd");
    private static SafeSimpleDateFormat timeFormat = new SafeSimpleDateFormat("HH:mm:ss");
    private static final Pattern inputVar = Pattern.compile("([^a-zA-Z0-9_.]|^)input[.]([a-zA-Z0-9_]+)", Pattern.CASE_INSENSITIVE);

    private static final ThreadLocal<Deque<StackEntry>> executionStack = new ThreadLocal<Deque<StackEntry>>()
    {
        public Deque<StackEntry> initialValue()
        {
            return new ArrayDeque<StackEntry>();
        }
    };

    /**
     * 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();
        }
    }

  /**
   * 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();
        return cells.remove(getColumnsFromIds(coordinate));
    }

    /**
   * @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.
     */
    public boolean containsCell(final Map<String, Object> coordinate, final boolean all)
    {
        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> ruleColExecutionValue = new HashMap<Long, Object>();
        final Map<String, Integer> ruleAxisBindCount = new CaseInsensitiveMap<String, Integer>();
        boolean done = false;
        boolean anyExist = 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)
                    {
                        Object ruleValue = ruleColExecutionValue.get(boundColumn.id);
                        if (ruleValue == 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());
                            ruleValue = cmd.run(executionContext);
                            ruleColExecutionValue.put(boundColumn.id, ruleValue);

                            if (ruleValue != Boolean.FALSE && ruleValue != null)
                            {   // Rule fired
                                Integer count = ruleAxisBindCount.get(axisName);
                                ruleAxisBindCount.put(axisName, count == null ? 1 : count + 1);
                            }
                        }
                        // 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 (ruleValue != Boolean.FALSE && ruleValue != null)
                        {
                            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)
                    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 e)
        {
        }

        checkForRuleModeViolation(ruleAxisBindCount);
        return all || anyExist;
    }

    public boolean containsCellById(final Set<Long> coordinate)
    {
        return cells.containsKey(getColumnsFromIds(coordinate));
    }

  /**
   * 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();
        return cells.put(getColumnsFromIds(coordinate), 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)
    {
        return cells.get(getColumnsFromIds(coordinate));
    }

    /**
     * @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.
     * @param coordinate Map of axis names to single values to match against the
     * 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().
     * @param output Output map that can be written to by code that runs within ncube cells.
     * @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<Long> coord = new HashSet<Long>();

        for (final Map.Entry<Map<String, Column>, T> entry : hits.entrySet())
        {
            coord.clear();
            for (Column column : entry.getKey().values())
            {
                coord.add(column.id);
            }

            if (containsCellById(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);
        }
        else 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 an 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.
     */
    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
        {
            final Map<String, Object> coord = validateCoordinate(coordinate);

            // Form fully qualified cell lookup (NCube name + coordinate)
            // Add fully qualified coordinate to ThreadLocal execution stack
            final StackEntry entry = new StackEntry(name, coord);
            stackFrame.push(entry);
            pushed = true;
            final T retVal = executeCellById(idCoord, coord, 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.
     * @param coord Map coordinate referencing a cell to execute.
     * @param output Map that can be written by code executing in ncube cells.
     * @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;

        if (cellValue == null)
        {
            return null;
        }
        else if (cellValue instanceof CommandCell)
        {
            try
            {
                final CommandCell cmd = (CommandCell) cellValue;
                cellValue = (T) cmd.run(prepareExecutionContext(coord, output));
            }
            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);
            }
        }
        else if (cellValue.getClass().isArray())
        {   // If the content of a cell is an Object[], walk the elements in the
            // array and execute any elements that are commands.  Return a new Object[]
            // of the original values or command results if some elements were Commands.
            cellValue = (T) processArray(coord, output, cellValue);
        }

        return cellValue;
    }

    /**
     * Process the cell contents when it is an Object[].  This includes handling CommandCells
     * that may be within the Object[], and other Object[]'s that may be inside, and so on.
     * @return [] that was executed.
     */
    private Object processArray(final Map<String, Object> coord, final Map output, Object cellValue)
    {
        final int len = Array.getLength(cellValue);
        if (len > 0 && cellValue instanceof Object[])
        {
            final Object[] forReturn = new Object[len];

            for (int i=0; i < len; i++)
            {
                Object element = Array.get(cellValue, i);
                if (element instanceof CommandCell)
                {
                    CommandCell cmd = (CommandCell) element;
                    try
                    {
                        forReturn[i] = cmd.run(prepareExecutionContext(coord, output));
                    }
                    catch (Exception e)
                    {
                        throw new RuntimeException("Error occurred executing Object[" + i + "] containing CommandCell in NCube '" + name + "'\n" + stackToString(), e);
                    }
                }
                else if (element instanceof Object[])
                {   // Handle Object[][][]... of commands
                    forReturn[i] = processArray(coord, output, element);
                }
                else
                {
                    forReturn[i] = element;
                }
            }
            cellValue = forReturn;
        }
        return cellValue;
    }

    /**
     * Prepare the execution context by providing it with references to
     * important items like the input coordinate, output map, stack,
     * this (ncube), and the NCubeManager.
     */
    private 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("stack", getStackAsList());
        args.put("ncube", this);
        args.put("ncubeMgr", NCubeManager.class);
        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 CaseInsensitiveMap<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.
     * @param coordinate Map of axis names to single values to match against the
     * 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.
     * @param output Map that can be written to by the executing cells.
     * @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 CaseInsensitiveMap<String, Integer>();
        final Map<String, Object> ruleInfo = new CaseInsensitiveMap<String, 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 = ruleColExecutionValue.get(boundColumn.id);
                        if (ruleValue == 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, output);
                            ruleValue = cmd.run(executionContext);
                            ruleColExecutionValue.put(boundColumn.id, ruleValue);

                            if (ruleValue != Boolean.FALSE && ruleValue != null)
                            {   // Rule fired
                                Integer count = ruleAxisBindCount.get(axisName);
                                ruleAxisBindCount.put(axisName, count == null ? 1 : count + 1);
                            }
                        }
                        // 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 (ruleValue != Boolean.FALSE && ruleValue != null)
                        {
                            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("ruleStop", false);
        }
        catch (RuleStop e)
        {
            ruleInfo.put("ruleStop", true);
        }

        if (anyRuleAxes)
        {
            output.put("_rule", ruleInfo);
        }
        checkForRuleModeViolation(ruleAxisBindCount);
        return executedCells;
    }

    /**
     * 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
     * in ruleMode.
     * @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 CaseInsensitiveMap<String, List<Column>>();
        for (final Map.Entry<String, Axis> entry : axisList.entrySet())
        {
            final Axis axis = entry.getValue();
            final Comparable value = (Comparable) coord.get(entry.getKey());
            final List<Column> columns = new ArrayList<Column>();

            if (axis.getType() == AxisType.RULE)
            {   // For RULE axis, all possible columns must be added (they are tested later during execution)
                for (Column column : axis.getColumns())
                {
                    if (!column.isDefault())
                    {
                        columns.add(column);
                    }
                }
                coordinates.put(axis.getName(), columns);
            }
            else if (axis.isMultiMatch())
            {   // For a multiMatch axis, only the columns that match the input coordinate are returned
                coordinates.put(axis.getName(), axis.findColumns(value));
            }
            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 + "'");
                }
                columns.add(column);
                coordinates.put(axis.getName(), columns);
            }
        }

        return coordinates;
    }

    private static String[] getAxisNames(final Map<String, List<Column>> bindings)
    {
        final String[] axisNames = new String[bindings.keySet().size()];
        int idx = 0;
        for (String axisName : bindings.keySet())
        {
            axisNames[idx++] = axisName.toLowerCase();
        }
        return axisNames;
    }

    private static Map<String, Integer> getCountersPerAxis(final Map<String, List<Column>> bindings)
    {
        final Map<String, Integer> counters = new CaseInsensitiveMap<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.
     */
    private Set<Column> getColumnsFromIds(final Set<Long> coordinate)
    {
        // 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).
        Map<Long, Column> idsToCols = new HashMap<Long, Column>();
        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);
                    idsToCols.put(id, column);
                }
            }
        }

        // 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();
                long defColId = defCol.id;
                coordinate.add(defColId);
                idsToCols.put(defColId, defCol);
                axisNamesRef.remove(axisName);
            }
        }

        if (!axisNamesRef.isEmpty())
        {
            throw new IllegalArgumentException("Column IDs missing for the axes: " + axisNamesRef + ", NCube '" + name + "'");
        }

        final Set<Column> cols = new HashSet<Column>();
        for (final Long id : coordinate)
        {
            cols.add(idsToCols.get(id));
        }
        return cols;
    }

    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;
            }
        }
    }

    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;
    }

    public T getDefaultCellValue()
    {
      return defaultCellValue;
    }

    public void setDefaultCellValue(final T defaultCellValue)
    {
      this.defaultCellValue = defaultCellValue;
    }

    public void clearCells()
    {
      cells.clear();
    }

    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;
    }

    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);
    }

    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);
    }

    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();
    }

  /**
   * 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)
  {
    if (axisList.containsKey(axis.getName().toLowerCase()))
    {
      throw new IllegalArgumentException("An axis with the name '" + axis.getName()
          + "' already exists on NCube '" + name + "'");
    }

    cells.clear();
    axisList.put(axis.getName().toLowerCase(), 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(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());
                    }
                }

                for (String key : getScopeKeysFromCommandCells(cube.cells))
                {
                    requiredScope.add(key);
                }
                for (String key : getScopeKeysFromRuleAxes(cube))
                {
                    requiredScope.add(key);
                }

                // Add all referenced sub-cubes to the stack
                for (final String ncube : getReferencedCubeNames())
                {
                    stack.addFirst(NCubeManager.getCube(ncube, version));
                }
            }

            // Cache computed result for fast retrieval
            Set<String> reqScope = new TreeSet<String>(requiredScope);
            // Cache required scope for fast retrieval
            scopeCache.put(name, reqScope);
            return new CaseInsensitiveSet<String>(reqScope);        // Return modifiable copy (sorted order maintained)
        }
    }

    private 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.variableName' within columns on
     * a Rule axis.  Add 'variableName' as required scope key.
     * @param ncube NCube to search
     * @return Set<String> of required scope (coordinate) keys.
     */
    private 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 = 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)
    {
        HtmlFormatter formatter = new HtmlFormatter(this);
        return formatter.format(headers);
    }

    public String toString()
    {
        StringBuilder s = new StringBuilder();
        s.append("Table: ");
        s.append(getName());
        s.append("\n  defaultCellValue: ");
        if (defaultCellValue == null)
        {
          s.append(" no default");
        }
        else
        {
          s.append(getDefaultCellValue().toString());
        }
        s.append('\n');

        for (Axis axis : axisList.values())
        {
            s.append(axis);
        }

        s.append("Cells:\n");
        for (Map.Entry<Set<Column>, T> entry : cells.entrySet())
        {
            s.append('[');
            s.append(entry.getKey());
            s.append("]=");
            s.append(entry.getValue());
            s.append('\n');
        }
        return s.toString();
    }

    // ----------------------------
    // 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
        {
            String baseUrl = SystemUtilities.getExternalVariable("NCUBE_BASE_URL");
            if (StringUtilities.isEmpty(baseUrl))
            {
                baseUrl = "";
            }
            else if (!baseUrl.endsWith("/"))
            {
                baseUrl += "/";
            }

            Map<Object, Long> userIdToUniqueId = new CaseInsensitiveMap<Object, Long>();
            Map map = JsonReader.jsonToMaps(json);
            String cubeName = getString(map, "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.defaultCellValue = parseJsonValue(map.get("defaultCellValue"), null);
            ncube.ruleMode = getBoolean(map, "ruleMode");

            if (!(map.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) map.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 obj = (Map) item;
                String name = getString(obj, "name");
                AxisType type = AxisType.valueOf(getString(obj, "type"));
                boolean hasDefault = getBoolean(obj, "hasDefault");
                AxisValueType valueType = AxisValueType.valueOf(getString(obj, "valueType"));
                final int preferredOrder = getLong(obj, "preferredOrder").intValue();
                final Boolean multiMatch = getBoolean(obj, "multiMatch");
                Axis axis = new Axis(name, type, valueType, hasDefault, preferredOrder, multiMatch);
                ncube.addAxis(axis);

                if (!(obj.get("columns") instanceof JsonObject))
                {
                    throw new IllegalArgumentException("'columns' must be specified, axis '" + name + "', NCube '" + cubeName + "'");
                }
                JsonObject colMap = (JsonObject) obj.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 mapCol = (Map) col;
                    Object value = mapCol.get("value");
                    String colType = (String) mapCol.get("type");
                    Object id = parseJsonValue(mapCol.get("id"), colType);

                    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;
                        }
                    }
                    Column colAdded;

                    if (type == AxisType.DISCRETE || type == AxisType.NEAREST)
                    {
                        colAdded = ncube.addColumn(axis.getName(), (Comparable) parseJsonValue(value, colType));
                    }
                    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], colType);
                        Comparable high = (Comparable) parseJsonValue(rangeItems[1], colType);
                        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], colType);
                                Comparable high = (Comparable) parseJsonValue(rangeValues[1], colType);
                                Range range = new Range(low, high);
                                rangeSet.add(range);
                            }
                            else
                            {
                                rangeSet.add((Comparable)parseJsonValue(pt, colType));
                            }
                        }
                        colAdded = ncube.addColumn(axis.getName(), rangeSet);
                    }
                    else if (type == AxisType.RULE)
                    {
                        Object cmd = parseJsonValue(value, colType);
                        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);
                    }
                }
            }

            // Read cells
            if (!(map.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) map.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");
                Object v = parseJsonValue(cMap.get("value"), type);
                if (v == null)
                {
                    String url = (String) cMap.get("url");
                    if (StringUtilities.isEmpty(url))
                    {
                        String uri = (String) cMap.get("uri");
                        if (StringUtilities.isEmpty(uri))
                        {
                            throw new IllegalArgumentException("Cell must have 'value', 'url', or 'uri' to specify its content, NCube '" + cubeName + "'");
                        }
                        url = baseUrl + uri;
                    }

                    boolean cache = true;
                    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')");
                        }
                    }
                    CommandCell cmd;
                    if ("exp".equalsIgnoreCase(type))
                    {
                        cmd = new GroovyExpression("");
                    }
                    else if ("method".equalsIgnoreCase(type))
                    {
                        cmd = new GroovyMethod("");
                    }
                    else if ("template".equalsIgnoreCase(type))
                    {
                        cmd = new GroovyTemplate("");
                    }
                    else if ("string".equalsIgnoreCase(type))
                    {
                        cmd = new StringUrlCmd(cache);
                    }
                    else if ("binary".equalsIgnoreCase(type))
                    {
                        cmd = new BinaryUrlCmd(cache);
                    }
                    else
                    {
                        throw new IllegalArgumentException("url/uri can only be specified with 'exp', 'method', 'template', 'string', or 'binary' types");
                    }
                    cmd.setUrl(url);
                    v = cmd;
                }

                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));
                    }
                    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 type)
    {
        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());
            }
            return value;
        }
        else if (value instanceof Boolean)
        {
            return value;
        }
        else if (value instanceof String)
        {
            if (StringUtilities.isEmpty(type))
            {
                try
                {   // attempt to parse as String (to allow dates to be a fundamental supported type)
                    return datetimeFormat.parse((String)value);
                }
                catch (ParseException e)
                {
                    return value;
                }
            }

            if ("exp".equals(type))
            {
                return new GroovyExpression((String)value);
            }
            else if ("method".equals(type))
            {
                return new GroovyMethod((String) value);
            }
            else if ("date".equals(type))
            {
                try
                {
                    return dateFormat.parse((String)value);
                }
                catch (ParseException e)
                {
                    throw new IllegalArgumentException("Could not parse date: " + value, e);
                }
            }
            else if ("datetime".equals(type))
            {
                try
                {
                    return datetimeFormat.parse((String)value);
                }
                catch (ParseException e)
                {
                    throw new IllegalArgumentException("Could not parse datetime: " + value, e);
                }
            }
            else if ("time".equals(type))
            {
                try
                {
                    return timeFormat.parse((String)value);
                }
                catch (ParseException e)
                {
                    throw new IllegalArgumentException("Could not parse time: " + value, e);
                }
            }
            else if ("template".equals(type))
            {
                return new GroovyTemplate((String)value);
            }
            else if ("string".equals(type))
            {
                return value;
            }
            else if ("binary".equals(type))
            {   // convert hex string "10AF3F" as byte[]
                return StringUtilities.decode((String) value);
            }
            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], type);
            }
            return values;
        }
        else if (value instanceof Object[])
        {
            Object[] values = (Object[]) value;
            for (int i=0; i < values.length; i++)
            {
                values[i] = parseJsonValue(values[i], type);
            }
            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 (value == null)
            {
                if (thatCellValue != null)
                {
                    return false;
                }
            }
            else
            {
                if (!value.equals(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 += 3 * value.hashCode();
            }
        }

        return h1 + h2;
    }
}
TOP

Related Classes of com.cedarsoftware.ncube.NCube$StackEntry

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.