Package com.cedarsoftware.ncube

Source Code of com.cedarsoftware.ncube.NCube

package com.cedarsoftware.ncube;

import com.cedarsoftware.ncube.exception.CoordinateNotFoundException;
import com.cedarsoftware.util.ArrayUtilities;
import com.cedarsoftware.util.CaseInsensitiveMap;
import com.cedarsoftware.util.CaseInsensitiveSet;
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.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.lang.Math.abs;

/**
* 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 final Map<String, Map<String, Set>> scopeCacheValues = new ConcurrentHashMap<String, Map<String, Set>>();
    private transient NCubeManager manager;
    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)
    {
        this.name = name;
    }

    /**
     * Set the NCubeManager that was used to load this NCube.
     * @param mgr NCubeManager instance that was used to load / create this NCube.
     */
    void setManager(NCubeManager mgr)
    {
        manager = mgr;
    }

    /**
     * @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, List<Column>> bindings = getCoordinateKeys(validateCoordinate(coordinate), new HashMap());
        validateMultiMatchInOneDimensionOnly(bindings);
        final String[] axisNames = getAxisNames(bindings);
        final Map<String, Integer> counters = getCountersPerAxis(bindings);

        // Go through each axis and obtain Set of 'hit' columns on the axis
        final Set<Column> idCoord = new HashSet<Column>();
        boolean done = false;
        boolean anyExist = false;

        while (!done)
        {
            // Step #1 Create coordinate for current counter positions
            idCoord.clear();
            for (String axisName : axisNames)
            {
                final List<Column> cols = bindings.get(axisName);
                idCoord.add(cols.get(counters.get(axisName) - 1));
            }

            // Step #2 Determine if the cell exists, and whether we are looking for any or all
            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, bindings, axisNames.length - 1, axisNames);
        }
        return all || anyExist;   // This is correct.  If you don't understand, please contact John
    }

    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();
        }
        synchronized(scopeCacheValues)
        {
            scopeCacheValues.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 coord = new HashSet();

        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", manager);
        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, List<Column>> bindings = getCoordinateKeys(validateCoordinate(coordinate), output);
        validateMultiMatchInOneDimensionOnly(bindings);
        final String[] axisNames = getAxisNames(bindings);
        final Map<Map<String, Column>, T> executedCells = new LinkedHashMap<Map<String, Column>, T>();
        final Map<String, Integer> counters = getCountersPerAxis(bindings);
        final Set<Column> idCoord = new HashSet<Column>();
        boolean done = false;

        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 = bindings.get(axisName);
                final Column boundColumn = cols.get(counters.get(axisName) - 1);
                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
            executedCells.put(coord, getCellById(idCoord, coordinate, output));

            // Step #3 increment counters (variable radix increment)
            done = incrementVariableRadixCount(counters, bindings, axisNames.length - 1, axisNames);
        }
        return executedCells;
    }

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

    /**
     * When in 'ruleMode', a check is performed before getCells() / containsCells() APIs return that ensures
     * that if more than one cell is returned, then all of those cells must be along a single axis.  This
     * characteristic is highly desirable when executing a RuleCube, because it enforces (allows the user to
     * know) that the rule statements executed in the order intended (column order).
     */
    private void validateMultiMatchInOneDimensionOnly(final Map<String, List<Column>> bindings)
    {
        if (ruleMode)
        {
            int numDimsWithMultiMatch = 0;
            for (final List<Column> columns : bindings.values())
            {
                if (columns.size() > 1)
                {
                    numDimsWithMultiMatch++;
                }
            }

            if (numDimsWithMultiMatch > 1)
            {
                throw new IllegalStateException("NCube '" + name +
                    "' is in 'ruleMode', yet multiple cells were resolved in more than one dimension. ");
            }
        }
    }

    /**
     * Given a Set of 'supposed' column identifiers, return a Set of Columns.
     * This method ensures that enough column identifiers are passed in (at least
     * 1 per each axis). 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 HashSet<String>();
        final Set<String> allAxes = new HashSet<String>();

        // Bind all Longs to Columns on an axis.  Allow for additional columns to be specified,
        // but not more than one column ID per axis.  Also, too few can be supplied, if and
        // only if, the axes that are not bound too have a Default column (which will be chosen).
        for (final Axis axis : axisList.values())
        {
            final String axisName = axis.getName();
            allAxes.add(axisName);
            for (final Long id : coordinate)
            {
                if (axis.idToCol.containsKey(id))
                {
                    if (axisNamesRef.contains(axisName))
                    {
                        throw new IllegalArgumentException("Cannot have more than one column ID per axis, axis '" + axisName + "', NCube '" + name + "'");
                    }
                    axisNamesRef.add(axisName);
                }
            }
        }

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

        for (final String axisName : allAxes)
        {
            Axis axis = axisList.get(axisName.toLowerCase());
            if (axis.hasDefaultColumn())
            {
                coordinate.add(axis.getDefaultColumn().id);
                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)
        {
            final Column col = new Column(id, false);
            cols.add(col);
        }
        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;
    }

    /**
     * Fetch all the cells that the passed in coordinate matches.  More than one cell can be
     * returned when there is a 'mutliMatch column'.
     * @return Map, where the keys are the names of each axis, and the associated value
     * is a List of Column objects that matched on the given axis. An axis can have more than
     * one column match if an axis is in 'multiMatch' mode.
     */
    private Map<String, List<Column>> getCoordinateKeys(final Map<String, Object> coordinate, final Map output)
    {
        final 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 Object value = coordinate.get(entry.getKey());

            if (axis.getType() == AxisType.RULE)
            {
                coordinates.put(axis.getName(), findConditionalColumns(axis, coordinate, output));
            }
            else
            {
                final List<Column> columns = axis.findColumns((Comparable) value);
                if (columns.isEmpty())
                {
                    throw new CoordinateNotFoundException("Value '" + value + "' not found on axis '" + axis.getName() + "', NCube '" + name + "'");
                }
                coordinates.put(axis.getName(), columns);
            }
        }

        return coordinates;
    }

    private List<Column> findConditionalColumns(final Axis axis, final Map<String, Object> coordinate, final Map output)
    {
        final List<Column> columns = new ArrayList<Column>();

        for (Column column : axis.getColumns())
        {
            if (!column.isDefault())
            {
                CommandCell cmd = (CommandCell) column.getValue();
                Object condRet = cmd.run(prepareExecutionContext(coordinate, output));
                if (condRet != Boolean.FALSE && condRet != null)
                {
                    columns.add(column);
                }
            }
        }

        if (columns.isEmpty() && axis.hasDefaultColumn())
        {
            columns.add(axis.getDefaultColumn());
        }
        return columns;
    }

    /**
     * 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 void 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 + "'");
      }
      axis.addColumn(value);
        clearRequiredScopeCache();
    }

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

    /**
     * @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 + "'");
    }

        if (ruleMode && axis.isMultiMatch() && getNumMultiMatchAxis() > 0)
        {
            throw new IllegalArgumentException("Only 1 'multiMatch' axis can be added to an NCube in 'RuleMode'.  Axis '" +
                    axis.getName() + "', 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 '" + newName + "' 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(manager.getCube(ncube));
                }
            }

            // 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)
    {
        if (axisList.size() < 1)
        {
            return "<!DOCTYPE html>\n" +
                    "<html lang=\"en\">\n" +
                    "  <head>\n" +
                    "    <meta charset=\"UTF-8\">\n" +
                    "    <title>Empty NCube</title>\n" +
                    "  </head>\n" +
                    "  <body/>\n" +
                    "</html>";
        }

        String html = "<!DOCTYPE html>\n" +
                "<html lang=\"en\">\n" +
                "<head>\n" +
                " <meta charset=\"UTF-8\">\n" +
                " <title>NCube: " + name + "</title>\n" +
                " <style>\n" +
                "table\n" +
                "{\n" +
                "border-collapse:collapse;\n" +
                "}\n" +
                "table, td, th\n" +
                "{\n" +
                "border:1px solid black;\n" +
                "font-family: \"arial\",\"helvetica\", sans-serif;\n" +
                "font-size: small;\n" +
                "font-weight: 500;\n" +
                "padding: 2px;\n" +
                "}\n" +
                "td\n" +
                "{\n" +
                "color: black;\n" +
                "background: white;\n" +
                "text-align: center;\n" +
                "}\n" +
                "th\n" +
                "{\n" +
                "color: white;\n" +
                "}\n" +
                ".ncube-num\n" +
                "{\n" +
                "text-align: right;\n" +
                "}\n" +
                ".ncube-dead\n" +
                "{\n" +
                "background: #6495ED;\n" +
                "} \n" +
                ".ncube-head\n" +
                "{\n" +
                "background: #4D4D4D;\n" +
                "}  \n" +
                ".ncube-col\n" +
                "{\n" +
                "background: #929292;\n" +
                "}\n" +
                " </style>\n" +
                "</head>\n" +
                "<body>\n" +
                "<table border=\"1\">\n" +
                "<tr>\n";

        StringBuilder s = new StringBuilder();
        Object[] displayValues = getDisplayValues(headers);
        List<Axis> axes = (List<Axis>) displayValues[0];
        long height = (Long) displayValues[1];
        long width = (Long) displayValues[2];

        s.append(html);

        // Top row (special case)
        Axis topAxis = axes.get(0);
        List<Column> topColumns = topAxis.getColumns();
        final int topColumnSize = topColumns.size();
        final String topAxisName = topAxis.getName();

        if (axes.size() == 1)
        {   // Ensure that one dimension is vertically down the page
            s.append(" <th data-id=\"a" + topAxis.id +"\" class=\"ncube-head\">");
            s.append(topAxisName);
            s.append("</th>\n");
            s.append(" <th class=\"ncube-dead\"></th>\n");
            s.append("</tr>\n");
            Set<Long> coord = new LinkedHashSet<Long>();

            for (int i=0; i < width; i++)
            {
                s.append("<tr>\n");
                Column column = topColumns.get(i);
                s.append(" <th data-id=\"c" + column.id + "\" class=\"ncube-col\">");
                s.append(column.isDefault() ? "Default" : column.toString());
                coord.clear();
                coord.add(topColumns.get(i).id);
                s.append("</th>\n");
                buildCell(s, coord);
                s.append("</tr>\n");
            }
        }
        else
        {   // 2D+ shows as one column on the X axis and all other dimensions on the Y axis.
            int deadCols = axes.size() - 1;
            if (deadCols > 0)
            {
                s.append(" <th class=\"ncube-dead\" colspan=\"" + deadCols + "\"></th>\n");
            }
            s.append(" <th data-id=\"a" + topAxis.id + "\" class=\"ncube-head\" colspan=\"");
            s.append(topAxis.size());
            s.append("\">");
            s.append(topAxisName);
            s.append("</th>\n");
            s.append("</tr>\n");

            // Second row (special case)
            s.append("<tr>\n");
            Map<String, Long> rowspanCounter = new HashMap<String, Long>();
            Map<String, Long> rowspan = new HashMap<String, Long>();
            Map<String, Long> columnCounter = new HashMap<String, Long>();
            Map<String, List<Column>> columns = new HashMap<String, List<Column>>();
            Map<String, Long> coord = new HashMap<String, Long>();

            final int axisCount = axes.size();

            for (int i=1; i < axisCount; i++)
            {
                Axis axis = axes.get(i);
                String axisName = axis.getName();
                s.append(" <th data-id=\"a" + axis.id + "\" class=\"ncube-head\">");
                s.append(axisName);
                s.append("</th>\n");
                long colspan = 1;

                for (int j=i + 1; j < axisCount; j++)
                {
                    colspan *= axes.get(j).size();
                }

                rowspan.put(axisName, colspan);
                rowspanCounter.put(axisName, 0L);
                columnCounter.put(axisName, 0L);
                columns.put(axisName, axis.getColumns());
            }

            for (Column column : topColumns)
            {
                s.append(" <th data-id=\"c" + column.id + "\" class=\"ncube-col\">");
                s.append(column.toString());
                s.append("</th>\n");
            }

            if (topAxis.size() != topColumnSize)
            {
                s.append(" <th class=\"ncube-col\">Default</th>");
            }

            s.append("</tr>\n");

            // The left column headers and cells
            for (long h=0; h < height; h++)
            {
                s.append("<tr>\n");
                // Column headers for the row
                for (int i=1; i < axisCount; i++)
                {
                    Axis axis = axes.get(i);
                    String axisName = axis.getName();
                    Long count = rowspanCounter.get(axisName);

                    if (count == 0)
                    {
                        Long colIdx = columnCounter.get(axisName);
                        Column column = columns.get(axisName).get(colIdx.intValue());
                        coord.put(axisName, column.id);
                        long span = rowspan.get(axisName);

                        if (span == 1)
                        {   // drop rowspan tag since rowspan="1" is redundant and wastes space in HTML
                            // Use column's ID as TH element's ID
                            s.append(" <th data-id=\"c" + column.id + "\" class=\"ncube-col\">");
                        }
                        else
                        {   // Need to show rowspan attribute
                            // Use column's ID as TH element's ID
                            s.append(" <th data-id=\"c" + column.id + "\" class=\"ncube-col\" rowspan=\"");
                            s.append(span);
                            s.append("\">");
                        }
                        s.append(column.toString());
                        s.append("</th>\n");

                        // Increment column counter
                        colIdx++;
                        if (colIdx >= axis.size())
                        {
                            colIdx = 0L;
                        }
                        columnCounter.put(axisName, colIdx);
                    }
                    // Increment row counter (counts from 0 to rowspan of subordinate axes)
                    count++;
                    if (count >= rowspan.get(axisName))
                    {
                        count = 0L;
                    }
                    rowspanCounter.put(axisName, count);
                }

                // Cells for the row
                for (int i=0; i < width; i++)
                {
                    coord.put(topAxisName, topColumns.get(i).id);
                    // Other coordinate values are set above this for-loop
                    buildCell(s, new LinkedHashSet<Long>(coord.values()));
                }

                s.append("</tr>\n");
            }
        }

        s.append("</table>\n");
        s.append("</body>\n");
        s.append("</html>");
        return s.toString();
    }

    private void buildCell(StringBuilder s, Set<Long> coord)
    {
        Object cellValue = getCellByIdNoExecute(coord);
        if (cellValue != null)
        {
            String strCell;
            if (cellValue instanceof Date || cellValue instanceof Number)
            {
                strCell = Column.formatDiscreteValue((Comparable) cellValue);
            }
            else if (cellValue.getClass().isArray())
            {
                try
                {
                    strCell = JsonWriter.objectToJson(cellValue);
                }
                catch (IOException e)
                {
                    throw new IllegalStateException("Error with simple JSON format", e);
                }
            }
            else
            {
                strCell = cellValue.toString();
            }

            String id = "k" + setToString(coord);
            s.append(cellValue instanceof Number ? " <td data-id=\"" + id + "\" class=\"ncube-num\">" : " <td data-id=\"" + id + "\">");
            s.append(strCell);
            s.append("</td>\n");
        }
        else
        {
            s.append(" <td></td>\n");
        }
    }

    private static String setToString(Set<Long> set)
    {
        StringBuilder s = new StringBuilder();
        Iterator<Long> i = set.iterator();

        while (i.hasNext())
        {
            s.append(i.next());
            if (i.hasNext())
            {
                s.append('.');
            }
        }
        return s.toString();
    }

    /**
     * Calculate import values needed to display an NCube.
     * @return Object[], where element 0 is a List containing the axes
     * where the first axis (element 0) is the axis to be displayed at the
     * top and the rest are the axes sorted smallest to larges.  Element 1
     * of the returned object array is the height of the cells (how many
     * rows it would take to display the entire ncube). Element 2 is the
     * width of the cell matrix (the number of columns would it take to display
     * the cell portion of the NCube).
     */
    private Object[] getDisplayValues(String ... headers)
    {
        if (headers == null)
        {
            headers = new String[]{};
        }
        Map headerStrings = new CaseInsensitiveMap();
        for (String header : headers)
        {
            headerStrings.put(header, null);
        }
        // Step 1. Sort axes from smallest to largest.
        // Hypercubes look best when the smaller axes are on the inside, and the larger axes are on the outside.
        List<Axis> axes = new ArrayList<Axis>(getAxes());
        Collections.sort(axes, new Comparator<Axis>()
        {
            public int compare(Axis a1, Axis a2)
            {
                return a2.size() - a1.size();
            }
        });

        // Step 2.  Now find an axis that is a good candidate for the single (top) axis.  This would be an axis
        // with the number of columns closest to 12.
        int smallestDelta = Integer.MAX_VALUE;
        int candidate = -1;
        int count = 0;

        for (Axis axis : axes)
        {
            if (headerStrings.keySet().contains(axis.getName()))
            {
                candidate = count;
                break;
            }
            int delta = abs(axis.size() - 12);
            if (delta < smallestDelta)
            {
                smallestDelta = delta;
                candidate = count;
            }
            count++;
        }

        // Step 3. Compute cell area size
        Axis top = axes.remove(candidate);
        axes.add(0, top);   // Top element is now first.
        long width = axes.get(0).size();
        long height = 1;
        final int len = axes.size();

        for (int i=1; i < len; i++)
        {
            height = axes.get(i).size() * height;
        }

        return new Object[] {axes, height, width};
    }

    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.  For example, if an ncube cell was anything
     * other than a String, Double, Boolean, Date, or null, it could
     * not be specified.  Same with column values.  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' 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<Long, Long> userIdToUniqueId = new HashMap<Long, 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 = mapCol.get("id");

                    if (value == null)
                    {
                        throw new IllegalArgumentException("Missing 'value' field on column or it is null, axis '" + name + "', NCube '" + cubeName + "'");
                    }

                    Column colAdded;

                    if (type == AxisType.DISCRETE || type == AxisType.NEAREST)
                    {
                        colAdded = axis.addColumn((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 = axis.addColumn(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 = axis.addColumn(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 = axis.addColumn((CommandCell)cmd);
                    }
                    else
                    {
                        throw new IllegalArgumentException("Unsupported Axis Type '" + type + "' for simple JSON input, axis '" + name + "', NCube '" + cubeName + "'");
                    }

                    if (id instanceof Long)
                    {
                        long sysId = colAdded.getId();
                        userIdToUniqueId.put((Long)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");
        }
    }
}
TOP

Related Classes of com.cedarsoftware.ncube.NCube

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.