blob: f2a8cdf44d2869b1ebd5107467df2e93cf853fe4 [file] [log] [blame]
/*
* 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) 2014 AT&T Knowledge Ventures
* Unpublished and Not for Publication
* All Rights Reserved
*/
package org.apache.openaz.xacml.pdp.test.policy;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.Marshaller;
import oasis.names.tc.xacml._3_0.core.schema.wd_17.AttributeType;
import oasis.names.tc.xacml._3_0.core.schema.wd_17.AttributeValueType;
import oasis.names.tc.xacml._3_0.core.schema.wd_17.AttributesType;
import oasis.names.tc.xacml._3_0.core.schema.wd_17.ObjectFactory;
import oasis.names.tc.xacml._3_0.core.schema.wd_17.PolicySetType;
import oasis.names.tc.xacml._3_0.core.schema.wd_17.PolicyType;
import oasis.names.tc.xacml._3_0.core.schema.wd_17.RequestType;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.ParseException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.openaz.xacml.api.AttributeValue;
import org.apache.openaz.xacml.api.DataType;
import org.apache.openaz.xacml.api.DataTypeException;
import org.apache.openaz.xacml.api.DataTypeFactory;
import org.apache.openaz.xacml.api.Identifier;
import org.apache.openaz.xacml.api.XACML3;
import org.apache.openaz.xacml.pdp.test.TestBase;
import org.apache.openaz.xacml.std.IdentifierImpl;
import org.apache.openaz.xacml.util.FactoryException;
import org.apache.openaz.xacml.util.XACMLObjectCopy;
import org.apache.openaz.xacml.util.XACMLPolicyAggregator;
import org.apache.openaz.xacml.util.XACMLPolicyScanner;
import org.apache.openaz.xacml.util.XACMLProperties;
/**
* This class reads the policy in and extracts all the attributes and their values that is contained in the
* Policy. It then generates a request every single combination of attributes found. The attributes mostly
* come from the Target Match elements, since they have both an attribute designator/selector matched with an
* attribute value.
*/
public class TestPolicy extends TestBase {
private static Log logger = LogFactory.getLog(TestPolicy.class);
private boolean skip;
private Path policy;
private XACMLPolicyAggregator aggregator = new XACMLPolicyAggregator();
private long index;
//
// Our command line parameters
//
public static final String OPTION_POLICY = "policy";
public static final String OPTION_SKIP_GENERATE = "skip";
static {
options.addOption(new Option(OPTION_POLICY, true, "Path to the policy file."));
options.addOption(new Option(OPTION_SKIP_GENERATE, false, "Skip generating requests."));
}
public class FlattenerObject {
Identifier category;
Identifier datatype;
Identifier attribute;
Set<AttributeValue<?>> values;
}
/**
* This application exercises a policy by producing ALL the possible request combinations for a policy.
* -policy Path to a policy file
*
* @param args
* @throws HelpException
* @throws org.apache.commons.cli.ParseException
* @throws java.net.MalformedURLException
*/
public TestPolicy(String[] args) throws MalformedURLException, ParseException, HelpException {
super(args);
}
/*
* Look for the -policy command line argument. This application needs a pointer to a specific policy in
* order to run. (non-Javadoc)
* @see org.apache.openaz.xacml.pdp.test.TestBase#parseCommands(java.lang.String[])
*/
@Override
protected void parseCommands(String[] args) throws ParseException, MalformedURLException, HelpException {
//
// Have our super do its job
//
super.parseCommands(args);
//
// Look for the policy option
//
CommandLine cl;
cl = new GnuParser().parse(options, args);
if (cl.hasOption(OPTION_POLICY)) {
this.policy = Paths.get(cl.getOptionValue(OPTION_POLICY));
//
// Ensure it exists
//
if (Files.notExists(this.policy)) {
throw new ParseException("Policy file does not exist.");
}
} else {
throw new ParseException("You need to specify the policy file to be used.");
}
if (cl.hasOption(OPTION_SKIP_GENERATE)) {
this.skip = true;
} else {
this.skip = false;
}
}
/*
* We override this method because here is where we want to scan the policy and aggregate all the
* attributes that are defined within the policy. This routine will then dump all the possible requests
* into the requests sub-directory. Thus, when this method returns the TestBase can proceed to iterate
* each generated request and run it against the PDP engine. (non-Javadoc)
* @see org.apache.openaz.xacml.pdp.test.TestBase#configure()
*/
@Override
protected void configure() throws FactoryException {
//
// Have our base class do its thing
//
super.configure();
//
// Setup where the PDP can find the policy
//
System.setProperty(XACMLProperties.PROP_ROOTPOLICIES, "policy");
System.setProperty("policy.file", this.policy.toString());
//
// Determine if they want us to skip generation. This helps when a huge number of
// requests will get generated for a policy and can take some time to do so. The user
// can generate the requests once and then start testing a policy against the requests. Thus,
// the attributes never changed but the policy logic did (saves time).
//
if (this.skip) {
return;
}
//
// Now we will scan the policy and get all the attributes.
//
XACMLPolicyScanner scanner = new XACMLPolicyScanner(this.policy, this.aggregator);
//
// The scanner returns us a policy object
//
Object policyObject = scanner.scan();
//
// Just dump some info
//
if (policyObject instanceof PolicySetType) {
logger.info("Creating requests for policyset: " + ((PolicySetType)policyObject).getDescription());
} else if (policyObject instanceof PolicyType) {
logger.info("Creating requests for policy: " + ((PolicyType)policyObject).getDescription());
}
//
// Call the function to create the requests
//
if (policyObject != null) {
this.createRequests();
}
logger.info("Completed Generating requests.");
}
@SuppressWarnings("unchecked")
protected void createRequests() {
//
// Clear out our request directory
//
this.removeRequests();
//
// Get our map
//
Map<Identifier, Map<Identifier, Map<Identifier, Set<AttributeValue<?>>>>> attributeMap = this.aggregator
.getAttributeMap();
//
// We're going to create an initial flat list of requests for each unique attribute ID. Unique being
// the
// category, datatype and attribute id.
//
// By flattening the list, it makes it easier to then generate all the combinations of possible
// requests.
//
List<FlattenerObject> attributes = new ArrayList<FlattenerObject>();
//
// Iterate through all the maps, we are going to flatten it
// out into an array list.
//
for (Map.Entry<Identifier, Map<Identifier, Map<Identifier, Set<AttributeValue<?>>>>> categoryEntry : attributeMap
.entrySet()) {
String category = categoryEntry.getKey().toString();
if (logger.isDebugEnabled()) {
logger.debug("Category: " + category);
}
Map<Identifier, Map<Identifier, Set<AttributeValue<?>>>> datatypeMap = categoryEntry.getValue();
for (Map.Entry<Identifier, Map<Identifier, Set<AttributeValue<?>>>> datatypeEntry : datatypeMap
.entrySet()) {
String datatype = datatypeEntry.getKey().toString();
if (logger.isDebugEnabled()) {
logger.debug("\tData Type: " + datatype);
}
Map<Identifier, Set<AttributeValue<?>>> attributeIDMap = datatypeEntry.getValue();
for (Map.Entry<Identifier, Set<AttributeValue<?>>> attributeIDEntry : attributeIDMap
.entrySet()) {
String attributeID = attributeIDEntry.getKey().toString();
if (logger.isDebugEnabled()) {
logger.debug("\t\tAttribute ID: " + attributeID);
}
Set<AttributeValue<?>> attributeValueSet = attributeIDEntry.getValue();
//
// Sanity check to see if there are any values. Sometimes there isn't if an attribute
// is a designator that is part of a condition or variable.
//
if (attributeValueSet.isEmpty()) {
if (logger.isDebugEnabled()) {
logger
.debug("No values for attribute " + attributeIDEntry.getKey().stringValue());
}
//
// Check for the boolean datatype, in that case we can safely
// assume the true/false are ALL the possible values.
//
if (!datatypeEntry.getKey().equals(XACML3.ID_DATATYPE_BOOLEAN)) {
//
// Not boolean, so skip it
//
continue;
}
if (logger.isDebugEnabled()) {
logger.debug("No values but its a boolean datatype, we will include it anyway.");
}
}
//
// Create our flattener object
//
FlattenerObject flat = new FlattenerObject();
flat.category = categoryEntry.getKey();
flat.datatype = datatypeEntry.getKey();
flat.attribute = attributeIDEntry.getKey();
flat.values = new HashSet<AttributeValue<?>>();
if (datatypeEntry.getKey().equals(XACML3.ID_DATATYPE_BOOLEAN)) {
//
// There are only 2 possible values, true or false
//
flat.values.add(this.createAttributeValue(flat.datatype, true));
flat.values.add(this.createAttributeValue(flat.datatype, false));
} else {
flat.values.addAll(attributeValueSet);
}
attributes.add(flat);
}
}
}
if (attributes.size() <= 1) {
//
// Only one attribute, why bother
//
logger.info("Not enough attributes in policy: " + attributes.size());
return;
}
/*
* PLD work more on this later. This combinatorial formula is only accurate if each attribute has one
* value.
*/
if (logger.isDebugEnabled()) {
//
// This isn't really accurate, if an attribute has more than one value
//
logger.debug(attributes.size() + " will generate "
+ computePossibleCombinations(attributes.size()));
}
this.index = 1;
for (int i = 0; i < attributes.size(); i++) {
FlattenerObject flat = attributes.get(i);
for (AttributeValue<?> value : flat.values) {
//
// Create a basic request object for just that attribute value.
//
RequestType request = new RequestType();
//
AttributesType attrs = new AttributesType();
attrs.setCategory(flat.category.stringValue());
request.getAttributes().add(attrs);
//
AttributeType attr = new AttributeType();
attr.setAttributeId(flat.attribute.stringValue());
attrs.getAttribute().add(attr);
//
AttributeValueType val = new AttributeValueType();
val.setDataType(flat.datatype.stringValue());
if (value.getValue() instanceof Collection) {
val.getContent().addAll((Collection<? extends Object>)value.getValue());
} else {
val.getContent().add(value.getValue().toString());
}
//
attr.getAttributeValue().add(val);
//
// Dump it out
//
this.writeRequest(request);
//
// Initiate recursive call to add other attributes to the request
//
this.recursivelyGenerateRequests(request, i + 1, attributes);
}
}
}
protected void recursivelyGenerateRequests(RequestType request, int i, List<FlattenerObject> attributes) {
if (logger.isTraceEnabled()) {
logger.trace("recursiveGenerate index: " + index + " i: " + i);
}
for (; i < attributes.size(); i++) {
FlattenerObject flat = attributes.get(i);
for (AttributeValue<?> value : flat.values) {
//
// Make a copy of the request
//
RequestType copyRequest = XACMLObjectCopy.deepCopy(request);
//
// Create the value object
//
AttributeValueType newValue = new AttributeValueType();
newValue.setDataType(flat.datatype.stringValue());
if (value.getValue() instanceof Collection) {
for (Object v : (Collection<?>)value.getValue()) {
newValue.getContent().add(v.toString());
}
} else {
newValue.getContent().add(value.getValue().toString());
}
//
// Add the value to the request
//
this.addAttribute(copyRequest, flat.category.stringValue(), flat.attribute.stringValue(),
newValue);
//
// Now write it out
//
this.writeRequest(copyRequest);
//
// Recursively go through the rest of the attributes
//
this.recursivelyGenerateRequests(copyRequest, i + 1, attributes);
}
}
}
public static long computePossibleCombinations(long numberOfAttributes) {
long num = 0;
for (long i = numberOfAttributes; i > 0; i--) {
num += computeCombinations(numberOfAttributes, i);
}
return num;
}
public static long computeFactorial(long n) {
long fact = 1;
for (long i = 1; i <= n; i++) {
fact *= i;
}
return fact;
}
public static long computePermutationsWithoutRepetition(long n, long r) {
//
// n!
// ---------
// (n - r)!
//
long nPrime = 1;
long n_rPrime = 1;
for (long i = n; i > 1; i--) {
nPrime *= i;
}
for (long i = (n - r); i > 1; i--) {
n_rPrime *= i;
}
return nPrime / n_rPrime;
}
public static long computeCombinations(long n, long r) {
//
// n!
// -----------
// r! * (n-r)!
//
long nPrime = 1;
long rPrime = 1;
long n_rPrime = 1;
for (long i = n; i > 1; i--) {
nPrime *= i;
}
for (long i = r; i > 1; i--) {
rPrime *= i;
}
for (long i = (n - r); i > 1; i--) {
n_rPrime *= i;
}
return nPrime / (rPrime * n_rPrime);
}
protected Set<AttributeValue<?>> getAttributeValues(RequestType request) {
//
// Get our map
//
Map<Identifier, Map<Identifier, Map<Identifier, Set<AttributeValue<?>>>>> attributeMap = this.aggregator
.getAttributeMap();
//
// Find the attribute
//
AttributesType attrs = request.getAttributes().get(0);
Map<Identifier, Map<Identifier, Set<AttributeValue<?>>>> categoryMap = attributeMap
.get(new IdentifierImpl(attrs.getCategory()));
if (categoryMap != null) {
AttributeType a = attrs.getAttribute().get(0);
Map<Identifier, Set<AttributeValue<?>>> datatypeMap = categoryMap.get(new IdentifierImpl(a
.getAttributeValue().get(0).getDataType()));
if (datatypeMap != null) {
Set<AttributeValue<?>> values = datatypeMap.get(new IdentifierImpl(a.getAttributeId()));
if (values != null) {
return values;
}
}
}
return Collections.emptySet();
}
protected AttributeValue<?> createAttributeValue(Identifier datatype, Object value) {
DataTypeFactory dataTypeFactory = null;
try {
dataTypeFactory = DataTypeFactory.newInstance();
if (dataTypeFactory == null) {
logger.error("Could not create data type factory");
return null;
}
} catch (FactoryException e) {
logger.error("Can't get Data type Factory: " + e.getLocalizedMessage());
return null;
}
DataType<?> dataTypeExtended = dataTypeFactory.getDataType(datatype);
if (dataTypeExtended == null) {
logger.error("Unknown datatype: " + datatype);
return null;
}
try {
return dataTypeExtended.createAttributeValue(value);
} catch (DataTypeException e) {
logger.error(e);
}
return null;
}
protected void removeRequests() {
//
// Delete any existing request files that we generate. i.e. Have the Unknown in the file name.
//
try {
Files.walkFileTree(Paths.get(this.directory.toString(), "requests"),
new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
//
// Sanity check the file name
//
Matcher matcher = pattern.matcher(file.getFileName().toString());
if (matcher.matches()) {
try {
//
// Pull what this request is supposed to be
//
String group = null;
int count = matcher.groupCount();
if (count >= 1) {
group = matcher.group(count - 1);
}
//
// Send it
//
if (group.equals("Unknown")) {
//
// Remove the file
//
Files.delete(file);
}
} catch (Exception e) {
logger.error(e);
e.printStackTrace();
}
}
return super.visitFile(file, attrs);
}
});
} catch (IOException e) {
logger.error("Failed to removeRequests from " + this.directory + " " + e);
}
}
protected void addRequests(RequestType request, List<RequestType> requests, int index) {
for (RequestType req : requests) {
//
// There really should only be one attribute
//
for (AttributesType attrs : req.getAttributes()) {
for (AttributeType attr : attrs.getAttribute()) {
for (AttributeValueType value : attr.getAttributeValue()) {
if (this.addAttribute(request, attrs.getCategory(), attr.getAttributeId(), value)) {
this.writeRequest(request);
}
}
}
}
}
}
/**
* Writes the request into the "requests" sub-directory, relative to the value of the "directory" setup
* during initialization. Writing the requests out allows one to go back and easily refer to the request
* when analyzing the responses generated after the PDP decide() call. Also, one can then use the
* generated requests into any test tools they wish to build.
*
* @param request - The request to be written.
*/
protected void writeRequest(RequestType request) {
if (logger.isTraceEnabled()) {
logger.trace("writeRequest: " + index);
}
try {
ObjectFactory of = new ObjectFactory();
JAXBElement<RequestType> requestElement = of.createRequest(request);
JAXBContext context = JAXBContext.newInstance(RequestType.class);
Marshaller m = context.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
Path outFile = Paths.get(this.directory, "requests",
String.format("Request.%06d.Unknown.xml", this.index));
m.marshal(requestElement, outFile.toFile());
} catch (Exception e) {
logger.error("Failed to write request: " + e.getMessage());
}
this.index++;
}
protected boolean addAttribute(RequestType request, String category, String id, AttributeValueType value) {
//
// See if the category exists
//
for (AttributesType attrs : request.getAttributes()) {
if (attrs.getCategory().equals(category)) {
//
// It does have the category. But does it have the attribute ID?
//
for (AttributeType attr : attrs.getAttribute()) {
if (attr.getAttributeId().equals(id)) {
//
// Yes, check for the same datatype
//
for (AttributeValueType val : attr.getAttributeValue()) {
if (val.getDataType().equals(value.getDataType())) {
//
// We have something already there
//
return false;
}
}
//
// The ID exists, but not the datatype
//
attr.getAttributeValue().add(value);
return true;
}
}
//
// If we get here, the ID does not exist
//
AttributeType attr = new AttributeType();
attr.setAttributeId(id);
attr.getAttributeValue().add(value);
attrs.getAttribute().add(attr);
return true;
}
}
//
// If we get here, the category does not exist. So add it in.
//
AttributesType attrs = new AttributesType();
attrs.setCategory(category);
AttributeType attr = new AttributeType();
attr.setAttributeId(id);
attr.getAttributeValue().add(value);
attrs.getAttribute().add(attr);
request.getAttributes().add(attrs);
return true;
}
public static void main(String[] args) {
try {
new TestPolicy(args).run();
} catch (ParseException | IOException | FactoryException e) {
logger.error(e);
} catch (HelpException e) {
}
}
/*
* // Map<CATEGORY, MAP<DATATYPE, MAP<ATTRIBUTEID, SET<VALUES>>>
* @SuppressWarnings("unchecked") private void generateRequests(Map<Identifier, Map<Identifier,
* Map<Identifier, Set<AttributeValue<?>>>>> categoryMap) { meta = new ArrayList<>(); for
* (Map.Entry<Identifier, Map<Identifier, Map<Identifier, Set<AttributeValue<?>>>>> categoryEntry :
* categoryMap.entrySet()) { String category = categoryEntry.getKey().toString();
* logger.debug("Category: " + category); Map<Identifier, Map<Identifier, Set<AttributeValue<?>>>>
* datatypeMap = categoryEntry.getValue(); for (Map.Entry<Identifier, Map<Identifier,
* Set<AttributeValue<?>>>> datatypeEntry : datatypeMap.entrySet()) { String datatype =
* datatypeEntry.getKey().toString(); logger.debug("\tData Type: " + datatype); Map<Identifier,
* Set<AttributeValue<?>>> attributeIDMap = datatypeEntry.getValue(); for (Map.Entry<Identifier,
* Set<AttributeValue<?>>> attributeIDEntry : attributeIDMap.entrySet()) { String attributeID =
* attributeIDEntry.getKey().toString(); logger.debug("\t\tAttribute ID: " + attributeID);
* Set<AttributeValue<?>> attributeValueSet = attributeIDEntry.getValue(); for (AttributeValue<?> value :
* attributeValueSet) { logger.debug("\t\t\tAttribute Value: " + value); } Iterator<AttributeValue<?>>
* iterator = attributeValueSet.iterator(); logger.debug("\t\t\t# of Attribute values: " +
* attributeValueSet.size()); meta.add(new Object[] {category, datatype, attributeID, iterator.next(),
* iterator, attributeValueSet}); } } } int count = 0; for (File file : output.toFile().listFiles()) {
* file.delete(); } do { RequestType request = new RequestType(); request.setCombinedDecision(false);
* request.setReturnPolicyIdList(false); List<AttributesType> attributesList = request.getAttributes();
* Map<String, AttributesType> category2Attribute= new HashMap<>(); for (int i = 0; i < meta.size(); i++)
* { Object[] record = meta.get(i); AttributesType attributes = null; if
* (category2Attribute.containsKey(record[0].toString())) attributes =
* category2Attribute.get(record[0].toString()); else { attributes = new AttributesType();
* attributes.setCategory(record[0].toString()); category2Attribute.put(record[0].toString(), attributes);
* attributesList.add(attributes); } // attributes.setId(record[2].toString()); List<AttributeType>
* attrList = attributes.getAttribute(); AttributeType attribute = new AttributeType();
* attribute.setAttributeId(record[2].toString()); List<AttributeValueType> valueList =
* attribute.getAttributeValue(); AttributeValue<?> attributeValue = (AttributeValue<?>) record[3];
* AttributeValueType value = new AttributeValueType();
* value.setDataType(attributeValue.getDataTypeId().toString()); List<Object> content =
* value.getContent(); content.addAll((Collection<? extends Object>) attributeValue.getValue());
* valueList.add(value); attrList.add(attribute); } try { ObjectFactory of = new ObjectFactory();
* JAXBElement<RequestType> requestElement = of.createRequest(request); JAXBContext context =
* JAXBContext.newInstance(RequestType.class); Marshaller m = context.createMarshaller();
* m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); m.marshal(requestElement,
* output.resolve("request" + count + ".xml").toFile()); // if (count == 0) {//Just send the first request
* to the engine StringWriter sw = new StringWriter(); m.marshal(requestElement, sw);
* logger.info(sw.toString()); EngineCaller engine = new LocalEngineCaller(); if (engine.startEngine(""))
* { String response = engine.decide(sw.toString(), "xml"); FileWriter writer = new
* FileWriter(output.resolve("response" + count + ".xml").toFile()); writer.write(response);
* writer.close(); logger.info("Response received: \n" + response); } // } } catch (Exception e) {
* e.printStackTrace(); } count++; } while (hasNextRequest()); logger.info("# of requests generated: " +
* count); } private boolean hasNextRequest() { int i = meta.size() - 1; Object[] record = meta.get(i);
* @SuppressWarnings("unchecked") Iterator<AttributeValue<?>> iterator = (Iterator<AttributeValue<?>>)
* record[4]; if (iterator.hasNext()) { record[3] = iterator.next(); } else { return
* recycleAttributeValue(i); } return true; }
* @SuppressWarnings("unchecked") private boolean recycleAttributeValue(int position) { boolean rc = true;
* if (position == 0) return false; Object[] record = meta.get(position); Set<AttributeValue<?>>
* attributeValueSet = (Set<AttributeValue<?>>) record[5]; Iterator<AttributeValue<?>> newIt =
* attributeValueSet.iterator(); record[4] = newIt; record[3] = newIt.next(); int i = position - 1;
* Object[] previous = meta.get(i); Iterator<AttributeValue<?>> preIt = (Iterator<AttributeValue<?>>)
* previous[4]; if (preIt.hasNext()) { previous[3] = preIt.next(); } else { rc = recycleAttributeValue(i);
* } return rc; }
*/
}