/* | |
* Licensed to the Apache Software Foundation (ASF) under one | |
* or more contributor license agreements. See the NOTICE file | |
* distributed with this work for additional information | |
* regarding copyright ownership. The ASF licenses this file | |
* to you under the Apache License, Version 2.0 (the | |
* "License"); you may not use this file except in compliance | |
* with the License. You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, | |
* software distributed under the License is distributed on an | |
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
* KIND, either express or implied. See the License for the | |
* specific language governing permissions and limitations | |
* under the License. | |
* | |
*/ | |
/* | |
* AT&T - PROPRIETARY | |
* THIS FILE CONTAINS PROPRIETARY INFORMATION OF | |
* AT&T AND IS NOT TO BE DISCLOSED OR USED EXCEPT IN | |
* ACCORDANCE WITH APPLICABLE AGREEMENTS. | |
* | |
* Copyright (c) 2013 AT&T Knowledge Ventures | |
* Unpublished and Not for Publication | |
* All Rights Reserved | |
*/ | |
package org.apache.openaz.xacml.pdp.std.functions; | |
import java.net.URI; | |
import java.util.ArrayList; | |
import java.util.Iterator; | |
import java.util.List; | |
import org.apache.openaz.xacml.api.AttributeValue; | |
import org.apache.openaz.xacml.api.DataType; | |
import org.apache.openaz.xacml.api.Identifier; | |
import org.apache.openaz.xacml.pdp.eval.EvaluationContext; | |
import org.apache.openaz.xacml.pdp.policy.Bag; | |
import org.apache.openaz.xacml.pdp.policy.ExpressionResult; | |
import org.apache.openaz.xacml.pdp.policy.FunctionArgument; | |
import org.apache.openaz.xacml.pdp.policy.FunctionArgumentAttributeValue; | |
import org.apache.openaz.xacml.pdp.policy.FunctionDefinition; | |
import org.apache.openaz.xacml.pdp.std.StdFunctionDefinitionFactory; | |
import org.apache.openaz.xacml.std.IdentifierImpl; | |
import org.apache.openaz.xacml.std.StdStatus; | |
import org.apache.openaz.xacml.std.StdStatusCode; | |
import org.apache.openaz.xacml.std.datatypes.DataTypes; | |
/** | |
* FunctionDefinitionSet implements {@link org.apache.openaz.xacml.pdp.policy.FunctionDefinition} to | |
* implement the XACML Set predicates as functions taking two arguments of <code>Bag</code> the same primitive | |
* type and returning either a <code>Boolean</code> or a <code>Bag</code> of the same primitive type. | |
* <P> | |
* The ipAddress, dnsName and xPathExpression do not have set functions defined for them in section 10.2.8 of | |
* the Release 3 XACML spec. In the first implementation of XACML we had separate files for each XACML | |
* Function. This release combines multiple Functions in fewer files to minimize code duplication. This file | |
* supports the following XACML codes: string-bag boolean-bag integer-bag double-bag time-bag date-bag | |
* dateTime-bag anyURI-bag hexBinary-bag base64Binary-bag dayTimeDuration-bag (version 1 and3) | |
* yearMonthDuration-bag (version 1 and 3) x500Name-bag rfc822Name-bag | |
* | |
* @param <I> the java class for the data type of the function Input arguments | |
* @param <O> the java class for the data type of the function Output | |
*/ | |
public class FunctionDefinitionHigherOrderBag<O, I> extends FunctionDefinitionBase<O, I> { | |
/** | |
* List of comparison operations. | |
*/ | |
public enum OPERATION { | |
ANY_OF, | |
ALL_OF, | |
ANY_OF_ANY, | |
ALL_OF_ANY, | |
ANY_OF_ALL, | |
ALL_OF_ALL, | |
MAP | |
}; | |
// the operation for this instance of the class | |
private OPERATION operation; | |
/** | |
* Constructor - need dataType input because of java Generic type-erasure during compilation. | |
* | |
* @param idIn | |
* @param dataTypeArgsIn | |
*/ | |
public FunctionDefinitionHigherOrderBag(Identifier idIn, DataType<O> dataTypeIn, | |
DataType<I> dataTypeArgsIn, OPERATION opIn) { | |
super(idIn, dataTypeIn, dataTypeArgsIn, ((opIn == OPERATION.MAP) ? true : false)); | |
operation = opIn; | |
} | |
@Override | |
public ExpressionResult evaluate(EvaluationContext evaluationContext, List<FunctionArgument> arguments) { | |
// simple argument check | |
if (arguments == null || arguments.size() < 2) { | |
return ExpressionResult.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, | |
this.getShortFunctionId() | |
+ " Expected at least 2 arguments, got " | |
+ ((arguments == null) ? "null" : arguments | |
.size()))); | |
} | |
// three functions have some things known about the arguments | |
if (operation == OPERATION.ALL_OF_ANY || operation == OPERATION.ANY_OF_ALL | |
|| operation == OPERATION.ALL_OF_ALL) { | |
if (arguments.size() != 3) { | |
return ExpressionResult.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, | |
this.getShortFunctionId() | |
+ " Expected 3 arguments, got " | |
+ arguments.size())); | |
} | |
// the 2nd & 3rd arguments must both be bags | |
if (arguments.get(1) == null || !arguments.get(1).isBag()) { | |
return ExpressionResult.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, | |
this.getShortFunctionId() | |
+ " 2nd argument must be bag, got '" | |
+ ((arguments.get(1) == null) | |
? "null" : this | |
.getShortDataTypeId(arguments | |
.get(1).getValue() | |
.getDataTypeId())) + "'")); | |
} | |
if (arguments.get(2) == null || !arguments.get(2).isBag()) { | |
return ExpressionResult.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, | |
this.getShortFunctionId() | |
+ " 3rd argument must be bag, got '" | |
+ ((arguments.get(2) == null) | |
? "null" : this | |
.getShortDataTypeId(arguments | |
.get(2).getValue() | |
.getDataTypeId())) + "'")); | |
} | |
} | |
// first argument is supposed to be a Function ID passed to us as an AnyURI | |
FunctionArgument functionIdArgument = arguments.get(0); | |
if (functionIdArgument == null || functionIdArgument.getValue() == null) { | |
return ExpressionResult.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, this | |
.getShortFunctionId() + " Predicate Function (first argument) was null")); | |
} | |
if (!functionIdArgument.getValue().getDataTypeId().equals(DataTypes.DT_ANYURI.getId())) { | |
return ExpressionResult.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, | |
this.getShortFunctionId() | |
+ " First argument expected URI, got " | |
+ functionIdArgument.getValue() | |
.getDataTypeId())); | |
} | |
Identifier functionId = new IdentifierImpl((URI)functionIdArgument.getValue().getValue()); | |
// look up the actual function definition based on that ID | |
StdFunctionDefinitionFactory fdf = new StdFunctionDefinitionFactory(); | |
FunctionDefinition predicate = fdf.getFunctionDefinition(functionId); | |
if (predicate == null) { | |
return ExpressionResult | |
.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, | |
this.getShortFunctionId() | |
+ " First argument was not URI of a function, got '" + functionId | |
+ "'")); | |
} | |
// in all cases except MAP, the predicate must return True/False | |
if (operation != OPERATION.MAP && !predicate.getDataTypeId().equals(DataTypes.DT_BOOLEAN.getId())) { | |
return ExpressionResult | |
.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, | |
this.getShortFunctionId() | |
+ " Predicate Function must return boolean, but '" | |
+ predicate.getId() + "' returns '" | |
+ this.getShortDataTypeId(predicate.getDataTypeId()))); | |
} | |
// The remaining arguments may be either bags or primitive types. | |
// We do not know what the primitive types will be, and do not concern ourselves about that here | |
// (the predicate function we just got and will call later will complain if they do not match its | |
// expectations). | |
// The predicate function will want things as FunctionAttributes, so we do not need to unwrap | |
// anything. | |
boolean bagSeen = false; | |
for (int i = 1; i < arguments.size(); i++) { | |
FunctionArgument argument = arguments.get(i); | |
if (argument == null) { | |
return ExpressionResult.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, | |
this.getShortFunctionId() | |
+ " Got null argument at index " + i)); | |
} | |
// force evaluation and check status | |
if (!argument.getStatus().isOk()) { | |
return ExpressionResult.newError(getFunctionStatus(argument.getStatus())); | |
} | |
// for bags, remember that we saw one; for non-bag primitives, check that the primitive value is | |
// not null | |
if (argument.isBag()) { | |
bagSeen = true; | |
} else { | |
if (argument.getValue() == null || argument.getValue().getValue() == null) { | |
return ExpressionResult | |
.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, this | |
.getShortFunctionId() + " Got null attribute at index " + i)); | |
} | |
} | |
} | |
// all functions require at least one bag | |
if (!bagSeen && operation != OPERATION.ANY_OF_ANY) { | |
return ExpressionResult.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, this | |
.getShortFunctionId() + " Did not get any Bag argument; must have at least 1")); | |
} | |
// arguments are ready for use | |
// list of arguments for passing to the predicate | |
List<FunctionArgument> predicateArguments = new ArrayList<FunctionArgument>(); | |
// for functions that take a single bag, which index is that bag at | |
int indexOfBagInOriginalArgs = -1; | |
// bag iterator | |
Iterator<AttributeValue<?>> bagIterator1; | |
Iterator<AttributeValue<?>> bagIterator2; | |
switch (operation) { | |
case ANY_OF: | |
// Copy the primitive arguments to the list for passing to the predicate, | |
// putting a place-holder in for the value from the (single) bag | |
for (int i = 1; i < arguments.size(); i++) { | |
predicateArguments.add(arguments.get(i)); | |
if (arguments.get(i).isBag()) { | |
if (indexOfBagInOriginalArgs == -1) { | |
indexOfBagInOriginalArgs = i; | |
} else { | |
// bag already found - we should have only one | |
return ExpressionResult | |
.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, | |
this.getShortFunctionId() | |
+ " must have only 1 bag; found one at index " | |
+ indexOfBagInOriginalArgs + " and another at " + i)); | |
} | |
} | |
} | |
// get each primitive value in turn | |
bagIterator1 = arguments.get(indexOfBagInOriginalArgs).getBag().getAttributeValues(); | |
while (bagIterator1.hasNext()) { | |
// all of the predicate arguments have been created except that the one from the bag needs to | |
// replace the place-holder in the list | |
predicateArguments.set(indexOfBagInOriginalArgs - 1, | |
new FunctionArgumentAttributeValue(bagIterator1.next())); | |
ExpressionResult res = predicate.evaluate(evaluationContext, predicateArguments); | |
if (!res.isOk()) { | |
return ExpressionResult | |
.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, this | |
.getShortFunctionId() + " Predicate error: " + res.getStatus().getStatusMessage())); | |
} | |
if ((Boolean)(res.getValue().getValue()) == Boolean.TRUE) { | |
return ER_TRUE; | |
} | |
} | |
return ER_FALSE; | |
case ALL_OF: | |
// Copy the primitive arguments to the list for passing to the predicate, | |
// putting a place-holder in for the value from the (single) bag | |
for (int i = 1; i < arguments.size(); i++) { | |
predicateArguments.add(arguments.get(i)); | |
if (arguments.get(i).isBag()) { | |
if (indexOfBagInOriginalArgs == -1) { | |
indexOfBagInOriginalArgs = i; | |
} else { | |
// bag already found - we should have only one | |
return ExpressionResult | |
.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, | |
this.getShortFunctionId() | |
+ " must have only 1 bag; found one at index " | |
+ indexOfBagInOriginalArgs + " and another at " + i)); | |
} | |
} | |
} | |
// get each primitive value in turn | |
bagIterator1 = arguments.get(indexOfBagInOriginalArgs).getBag().getAttributeValues(); | |
while (bagIterator1.hasNext()) { | |
// all of the predicate arguments have been created except that the one from the bag needs to | |
// replace the place-holder in the list | |
predicateArguments.set(indexOfBagInOriginalArgs - 1, | |
new FunctionArgumentAttributeValue(bagIterator1.next())); | |
ExpressionResult res = predicate.evaluate(evaluationContext, predicateArguments); | |
if (!res.isOk()) { | |
return ExpressionResult | |
.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, this | |
.getShortFunctionId() + " Predicate error: " + res.getStatus().getStatusMessage())); | |
} | |
if ((Boolean)(res.getValue().getValue()) == Boolean.FALSE) { | |
return ER_FALSE; | |
} | |
} | |
return ER_TRUE; | |
case ANY_OF_ANY: | |
// empty bags can give odd error messages, so check here and return something that makes more | |
// sense | |
for (int i = 1; i < arguments.size(); i++) { | |
if (arguments.get(i).isBag() && arguments.get(i).getBag().size() == 0) { | |
return ExpressionResult | |
.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, this | |
.getShortFunctionId() + " Bag is empty at index " + i)); | |
} | |
} | |
// This is different from all the other Higher-order bag functions because it can take an | |
// unbounded number of arguments any/all of which may be bags. | |
// (The others take either an unbounded number of args of which exactly 1 is a bag, or they take | |
// exactly 2 bags) | |
// To handle the possibility of multiple bags without knowing a priori how many there might be, | |
// we first create all possible lists of arguments to be passed to the predicate. | |
// This is done using a depth-first search of the total argument space. | |
List<List<FunctionArgument>> listOfPredicateLists = new ArrayList<List<FunctionArgument>>(); | |
/* | |
* Start the recursive append process | |
*/ | |
appendCrossProduct(new ArrayList<FunctionArgument>(), arguments.subList(1, arguments.size()), 0, | |
listOfPredicateLists); | |
// we now have all possible argument lists for the predicate to work on, so do the ANY operation | |
// now | |
for (List<FunctionArgument> predicateArgumentList : listOfPredicateLists) { | |
// all of the predicate arguments have been created except that the one from the bag needs to | |
// replace the place-holder in the list | |
ExpressionResult res = predicate.evaluate(evaluationContext, predicateArgumentList); | |
if (!res.isOk()) { | |
return ExpressionResult | |
.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, this | |
.getShortFunctionId() + " Predicate error: " + res.getStatus().getStatusMessage())); | |
} | |
if ((Boolean)(res.getValue().getValue()) == Boolean.TRUE) { | |
return ER_TRUE; | |
} | |
} | |
// if we get here then none of the combinations gave a TRUE result | |
return ER_FALSE; | |
case ALL_OF_ANY: | |
// TODO - it might be more efficient to extract all the attributes from the first bag and convert | |
// them to FunctionArguments just once, then use that list each time | |
// get the element from the 2nd bag that we want to check all elements from the 1st bag against | |
bagIterator2 = arguments.get(2).getBag().getAttributeValues(); | |
while (bagIterator2.hasNext()) { | |
FunctionArgument predicateArgument2 = new FunctionArgumentAttributeValue(bagIterator2.next()); | |
boolean allMatch = true; | |
// now look at every value of the first bag operating with the selected value from the 2nd | |
bagIterator1 = arguments.get(1).getBag().getAttributeValues(); | |
while (bagIterator1.hasNext()) { | |
predicateArguments.clear(); | |
predicateArguments.add(new FunctionArgumentAttributeValue(bagIterator1.next())); | |
predicateArguments.add(predicateArgument2); | |
ExpressionResult res = predicate.evaluate(evaluationContext, predicateArguments); | |
if (!res.isOk()) { | |
return ExpressionResult | |
.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, | |
this.getShortFunctionId() + " Predicate error: " | |
+ res.getStatus().getStatusMessage())); | |
} | |
if ((Boolean)(res.getValue().getValue()) == Boolean.FALSE) { | |
allMatch = false; | |
break; | |
} | |
} | |
if (allMatch) { | |
// wee found one value in bag2 that works (is TRUE) for all values in bag1 | |
return ER_TRUE; | |
} | |
// this value from bag2 did not work, so get the next one | |
} | |
// no value in bag2 worked for all values of bag1 | |
return ER_FALSE; | |
case ANY_OF_ALL: | |
// TODO - it might be more efficient to extract all the attributes from the 2nd bag and convert | |
// them to FunctionArguments just once, then use that list each time | |
// get the element from the 1st bag that we want to check all elements from the 1st bag against | |
bagIterator1 = arguments.get(1).getBag().getAttributeValues(); | |
while (bagIterator1.hasNext()) { | |
FunctionArgument predicateArgument1 = new FunctionArgumentAttributeValue(bagIterator1.next()); | |
boolean allMatch = true; | |
// now look at every value of the 2nd bag operating with the selected value from the first | |
bagIterator2 = arguments.get(2).getBag().getAttributeValues(); | |
while (bagIterator2.hasNext()) { | |
predicateArguments.clear(); | |
predicateArguments.add(predicateArgument1); | |
predicateArguments.add(new FunctionArgumentAttributeValue(bagIterator2.next())); | |
ExpressionResult res = predicate.evaluate(evaluationContext, predicateArguments); | |
if (!res.isOk()) { | |
return ExpressionResult | |
.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, | |
this.getShortFunctionId() + " Predicate error: " | |
+ res.getStatus().getStatusMessage())); | |
} | |
if ((Boolean)(res.getValue().getValue()) == Boolean.FALSE) { | |
allMatch = false; | |
break; | |
} | |
} | |
if (allMatch) { | |
// wee found one value in bag1 that works (is TRUE) for all values in bag2 | |
return ER_TRUE; | |
} | |
// this value from bag1 did not work, so get the next one | |
} | |
// no value in bag1 worked for all values of bag2 | |
return ER_FALSE; | |
case ALL_OF_ALL: | |
// TODO - it might be more efficient to extract all the attributes from the 2nd bag and convert | |
// them to FunctionArguments just once, then use that list each time | |
// get the element from the 1st bag that we want to check all elements from the 1st bag against | |
bagIterator1 = arguments.get(1).getBag().getAttributeValues(); | |
while (bagIterator1.hasNext()) { | |
FunctionArgument predicateArgument1 = new FunctionArgumentAttributeValue(bagIterator1.next()); | |
// now look at every value of the 2nd bag operating with the selected value from the first | |
bagIterator2 = arguments.get(2).getBag().getAttributeValues(); | |
while (bagIterator2.hasNext()) { | |
predicateArguments.clear(); | |
predicateArguments.add(predicateArgument1); | |
predicateArguments.add(new FunctionArgumentAttributeValue(bagIterator2.next())); | |
ExpressionResult res = predicate.evaluate(evaluationContext, predicateArguments); | |
if (!res.isOk()) { | |
return ExpressionResult | |
.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, | |
this.getShortFunctionId() + " Predicate error: " | |
+ res.getStatus().getStatusMessage())); | |
} | |
if ((Boolean)(res.getValue().getValue()) == Boolean.FALSE) { | |
return ER_FALSE; | |
} | |
} | |
// this value did not fail, so try the next | |
} | |
// everything in bag1 worked (was true) for everything in bag 2 | |
return ER_TRUE; | |
case MAP: | |
// Copy the primitive arguments to the list for passing to the predicate, | |
// putting a place-holder in for the value from the (single) bag | |
for (int i = 1; i < arguments.size(); i++) { | |
predicateArguments.add(arguments.get(i)); | |
if (arguments.get(i).isBag()) { | |
if (indexOfBagInOriginalArgs == -1) { | |
indexOfBagInOriginalArgs = i; | |
} else { | |
// bag already found - we should have only one | |
return ExpressionResult | |
.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, | |
this.getShortFunctionId() | |
+ " must have only 1 bag; found one at index " | |
+ indexOfBagInOriginalArgs + " and another at " + i)); | |
} | |
} | |
} | |
Bag outputBag = new Bag(); | |
// get each primitive value in turn | |
bagIterator1 = arguments.get(indexOfBagInOriginalArgs).getBag().getAttributeValues(); | |
while (bagIterator1.hasNext()) { | |
// all of the predicate arguments have been created except that the one from the bag needs to | |
// replace the place-holder in the list | |
predicateArguments.set(indexOfBagInOriginalArgs - 1, | |
new FunctionArgumentAttributeValue(bagIterator1.next())); | |
ExpressionResult res = predicate.evaluate(evaluationContext, predicateArguments); | |
if (!res.isOk()) { | |
return ExpressionResult | |
.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, this | |
.getShortFunctionId() + " Predicate error: " + res.getStatus().getStatusMessage())); | |
} | |
if (res.isBag()) { | |
return ExpressionResult | |
.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, | |
this.getShortFunctionId() | |
+ " Cannot put bag inside bag; predicate was '" | |
+ predicate.getId() + "'")); | |
} | |
outputBag.add(res.getValue()); | |
} | |
return ExpressionResult.newBag(outputBag); | |
} | |
// all cases should have been covered by above - should never get here | |
return ExpressionResult.newError(new StdStatus(StdStatusCode.STATUS_CODE_PROCESSING_ERROR, this | |
.getShortFunctionId() + " Could not evaluate Higher-Order Bag function " + operation)); | |
} | |
/** | |
* Performs the depth-first walk to generate argument lists. Needed by any-of-any because of the variable | |
* number of bags it might get. This code was salvaged from the R2 version of the product and adjusted to | |
* fit the new way of doing business. | |
* | |
* @param argListInProgress the current argument list being generated in this pass | |
* @param valueList the list of expression result values | |
* @param nPosition the position within the expression result values to use to append to the base argument | |
* list | |
* @param listArgLists the <code>List</code> where final argument lists are appended | |
*/ | |
private static void appendCrossProduct(List<FunctionArgument> argListInProgress, | |
List<FunctionArgument> valueList, int nPosition, | |
List<List<FunctionArgument>> listArgLists) { | |
/* | |
* Have we hit a leaf? | |
*/ | |
if (nPosition >= valueList.size()) { | |
List<FunctionArgument> copy = new ArrayList<FunctionArgument>(); | |
copy.addAll(argListInProgress); | |
listArgLists.add(copy); | |
return; | |
} | |
/* | |
* Check to see if the value at the current position is a primitive or a bag | |
*/ | |
FunctionArgument FunctionArgument = valueList.get(nPosition); | |
if (FunctionArgument.isBag() && FunctionArgument.getBag().getAttributeValues() != null | |
&& FunctionArgument.getBag().size() > 0) { | |
Iterator<AttributeValue<?>> iterBagValues = FunctionArgument.getBag().getAttributeValues(); | |
while (iterBagValues.hasNext()) { | |
AttributeValue<?> attributeValue = iterBagValues.next(); | |
FunctionArgument functionArgument = new FunctionArgumentAttributeValue(attributeValue); | |
argListInProgress.add(functionArgument); | |
appendCrossProduct(argListInProgress, valueList, nPosition + 1, listArgLists); | |
argListInProgress.remove(argListInProgress.size() - 1); | |
} | |
} else { | |
/* | |
* This is a simple value, so we can just append to the argListInProgress and continue the | |
* recursion | |
*/ | |
argListInProgress.add(FunctionArgument); | |
appendCrossProduct(argListInProgress, valueList, nPosition + 1, listArgLists); | |
argListInProgress.remove(argListInProgress.size() - 1); | |
} | |
} | |
} |