|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
package com.sun.org.apache.xml.internal.security.signature; |
|
|
|
import java.io.IOException; |
|
import java.io.OutputStream; |
|
import java.security.AccessController; |
|
import java.security.PrivilegedAction; |
|
import java.util.Base64; |
|
import java.util.HashSet; |
|
import java.util.Iterator; |
|
import java.util.Set; |
|
|
|
import com.sun.org.apache.xml.internal.security.algorithms.Algorithm; |
|
import com.sun.org.apache.xml.internal.security.algorithms.MessageDigestAlgorithm; |
|
import com.sun.org.apache.xml.internal.security.c14n.CanonicalizationException; |
|
import com.sun.org.apache.xml.internal.security.c14n.InvalidCanonicalizerException; |
|
import com.sun.org.apache.xml.internal.security.exceptions.XMLSecurityException; |
|
import com.sun.org.apache.xml.internal.security.signature.reference.ReferenceData; |
|
import com.sun.org.apache.xml.internal.security.signature.reference.ReferenceNodeSetData; |
|
import com.sun.org.apache.xml.internal.security.signature.reference.ReferenceOctetStreamData; |
|
import com.sun.org.apache.xml.internal.security.signature.reference.ReferenceSubTreeData; |
|
import com.sun.org.apache.xml.internal.security.transforms.InvalidTransformException; |
|
import com.sun.org.apache.xml.internal.security.transforms.Transform; |
|
import com.sun.org.apache.xml.internal.security.transforms.TransformationException; |
|
import com.sun.org.apache.xml.internal.security.transforms.Transforms; |
|
import com.sun.org.apache.xml.internal.security.transforms.params.InclusiveNamespaces; |
|
import com.sun.org.apache.xml.internal.security.utils.Constants; |
|
import com.sun.org.apache.xml.internal.security.utils.DigesterOutputStream; |
|
import com.sun.org.apache.xml.internal.security.utils.SignatureElementProxy; |
|
import com.sun.org.apache.xml.internal.security.utils.UnsyncBufferedOutputStream; |
|
import com.sun.org.apache.xml.internal.security.utils.XMLUtils; |
|
import com.sun.org.apache.xml.internal.security.utils.resolver.ResourceResolver; |
|
import com.sun.org.apache.xml.internal.security.utils.resolver.ResourceResolverException; |
|
import org.w3c.dom.Attr; |
|
import org.w3c.dom.Document; |
|
import org.w3c.dom.Element; |
|
import org.w3c.dom.Node; |
|
import org.w3c.dom.Text; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public class Reference extends SignatureElementProxy { |
|
|
|
|
|
public static final String OBJECT_URI = Constants.SignatureSpecNS + Constants._TAG_OBJECT; |
|
|
|
|
|
public static final String MANIFEST_URI = Constants.SignatureSpecNS + Constants._TAG_MANIFEST; |
|
|
|
|
|
|
|
*/ |
|
public static final int MAXIMUM_TRANSFORM_COUNT = 5; |
|
|
|
private boolean secureValidation; |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private static boolean useC14N11 = |
|
AccessController.doPrivileged((PrivilegedAction<Boolean>) |
|
() -> Boolean.getBoolean("com.sun.org.apache.xml.internal.security.useC14N11")); |
|
|
|
private static final com.sun.org.slf4j.internal.Logger LOG = |
|
com.sun.org.slf4j.internal.LoggerFactory.getLogger(Reference.class); |
|
|
|
private Manifest manifest; |
|
private XMLSignatureInput transformsOutput; |
|
|
|
private Transforms transforms; |
|
|
|
private Element digestMethodElem; |
|
|
|
private Element digestValueElement; |
|
|
|
private ReferenceData referenceData; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
protected Reference( |
|
Document doc, String baseURI, String referenceURI, Manifest manifest, |
|
Transforms transforms, String messageDigestAlgorithm |
|
) throws XMLSignatureException { |
|
super(doc); |
|
|
|
addReturnToSelf(); |
|
|
|
this.baseURI = baseURI; |
|
this.manifest = manifest; |
|
|
|
this.setURI(referenceURI); |
|
|
|
// important: The ds:Reference must be added to the associated ds:Manifest |
|
// or ds:SignedInfo _before_ the this.resolverResult() is called. |
|
// this.manifest.appendChild(this.constructionElement); |
|
// this.manifest.appendChild(this.doc.createTextNode("\n")); |
|
|
|
if (transforms != null) { |
|
this.transforms = transforms; |
|
appendSelf(transforms); |
|
addReturnToSelf(); |
|
} |
|
|
|
|
|
Algorithm digestAlgorithm = new Algorithm(getDocument(), messageDigestAlgorithm) { |
|
public String getBaseNamespace() { |
|
return Constants.SignatureSpecNS; |
|
} |
|
|
|
public String getBaseLocalName() { |
|
return Constants._TAG_DIGESTMETHOD; |
|
} |
|
}; |
|
|
|
digestMethodElem = digestAlgorithm.getElement(); |
|
|
|
appendSelf(digestMethodElem); |
|
addReturnToSelf(); |
|
|
|
digestValueElement = |
|
XMLUtils.createElementInSignatureSpace(getDocument(), Constants._TAG_DIGESTVALUE); |
|
|
|
appendSelf(digestValueElement); |
|
addReturnToSelf(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
protected Reference(Element element, String baseURI, Manifest manifest) throws XMLSecurityException { |
|
this(element, baseURI, manifest, true); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
protected Reference(Element element, String baseURI, Manifest manifest, boolean secureValidation) |
|
throws XMLSecurityException { |
|
super(element, baseURI); |
|
this.secureValidation = secureValidation; |
|
this.baseURI = baseURI; |
|
Element el = XMLUtils.getNextElement(element.getFirstChild()); |
|
|
|
if (el != null && Constants._TAG_TRANSFORMS.equals(el.getLocalName()) |
|
&& Constants.SignatureSpecNS.equals(el.getNamespaceURI())) { |
|
transforms = new Transforms(el, this.baseURI); |
|
transforms.setSecureValidation(secureValidation); |
|
if (secureValidation && transforms.getLength() > MAXIMUM_TRANSFORM_COUNT) { |
|
Object exArgs[] = { transforms.getLength(), MAXIMUM_TRANSFORM_COUNT }; |
|
|
|
throw new XMLSecurityException("signature.tooManyTransforms", exArgs); |
|
} |
|
el = XMLUtils.getNextElement(el.getNextSibling()); |
|
} |
|
|
|
digestMethodElem = el; |
|
if (digestMethodElem == null) { |
|
throw new XMLSecurityException("signature.Reference.NoDigestMethod"); |
|
} |
|
|
|
digestValueElement = XMLUtils.getNextElement(digestMethodElem.getNextSibling()); |
|
if (digestValueElement == null) { |
|
throw new XMLSecurityException("signature.Reference.NoDigestValue"); |
|
} |
|
this.manifest = manifest; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public MessageDigestAlgorithm getMessageDigestAlgorithm() throws XMLSignatureException { |
|
if (digestMethodElem == null) { |
|
return null; |
|
} |
|
|
|
String uri = digestMethodElem.getAttributeNS(null, Constants._ATT_ALGORITHM); |
|
|
|
if ("".equals(uri)) { |
|
return null; |
|
} |
|
|
|
if (secureValidation && MessageDigestAlgorithm.ALGO_ID_DIGEST_NOT_RECOMMENDED_MD5.equals(uri)) { |
|
Object exArgs[] = { uri }; |
|
|
|
throw new XMLSignatureException("signature.signatureAlgorithm", exArgs); |
|
} |
|
|
|
return MessageDigestAlgorithm.getInstance(getDocument(), uri); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public void setURI(String uri) { |
|
if (uri != null) { |
|
setLocalAttribute(Constants._ATT_URI, uri); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public String getURI() { |
|
return getLocalAttribute(Constants._ATT_URI); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public void setId(String id) { |
|
if (id != null) { |
|
setLocalIdAttribute(Constants._ATT_ID, id); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public String getId() { |
|
return getLocalAttribute(Constants._ATT_ID); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public void setType(String type) { |
|
if (type != null) { |
|
setLocalAttribute(Constants._ATT_TYPE, type); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public String getType() { |
|
return getLocalAttribute(Constants._ATT_TYPE); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public boolean typeIsReferenceToObject() { |
|
if (Reference.OBJECT_URI.equals(this.getType())) { |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public boolean typeIsReferenceToManifest() { |
|
if (Reference.MANIFEST_URI.equals(this.getType())) { |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private void setDigestValueElement(byte[] digestValue) { |
|
Node n = digestValueElement.getFirstChild(); |
|
while (n != null) { |
|
digestValueElement.removeChild(n); |
|
n = n.getNextSibling(); |
|
} |
|
|
|
String base64codedValue = Base64.getMimeEncoder().encodeToString(digestValue); |
|
Text t = createText(base64codedValue); |
|
|
|
digestValueElement.appendChild(t); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public void generateDigestValue() |
|
throws XMLSignatureException, ReferenceNotInitializedException { |
|
this.setDigestValueElement(this.calculateDigest(false)); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public XMLSignatureInput getContentsBeforeTransformation() |
|
throws ReferenceNotInitializedException { |
|
try { |
|
Attr uriAttr = |
|
getElement().getAttributeNodeNS(null, Constants._ATT_URI); |
|
|
|
ResourceResolver resolver = |
|
ResourceResolver.getInstance( |
|
uriAttr, this.baseURI, this.manifest.getPerManifestResolvers(), secureValidation |
|
); |
|
resolver.addProperties(this.manifest.getResolverProperties()); |
|
|
|
return resolver.resolve(uriAttr, this.baseURI, secureValidation); |
|
} catch (ResourceResolverException ex) { |
|
throw new ReferenceNotInitializedException(ex); |
|
} |
|
} |
|
|
|
private XMLSignatureInput getContentsAfterTransformation( |
|
XMLSignatureInput input, OutputStream os |
|
) throws XMLSignatureException { |
|
try { |
|
Transforms transforms = this.getTransforms(); |
|
XMLSignatureInput output = null; |
|
|
|
if (transforms != null) { |
|
output = transforms.performTransforms(input, os); |
|
this.transformsOutput = output; |
|
|
|
//this.transformsOutput.setSourceURI(output.getSourceURI()); |
|
} else { |
|
output = input; |
|
} |
|
|
|
return output; |
|
} catch (ResourceResolverException ex) { |
|
throw new XMLSignatureException(ex); |
|
} catch (CanonicalizationException ex) { |
|
throw new XMLSignatureException(ex); |
|
} catch (InvalidCanonicalizerException ex) { |
|
throw new XMLSignatureException(ex); |
|
} catch (TransformationException ex) { |
|
throw new XMLSignatureException(ex); |
|
} catch (XMLSecurityException ex) { |
|
throw new XMLSignatureException(ex); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public XMLSignatureInput getContentsAfterTransformation() |
|
throws XMLSignatureException { |
|
XMLSignatureInput input = this.getContentsBeforeTransformation(); |
|
cacheDereferencedElement(input); |
|
|
|
return this.getContentsAfterTransformation(input, null); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public XMLSignatureInput getNodesetBeforeFirstCanonicalization() |
|
throws XMLSignatureException { |
|
try { |
|
XMLSignatureInput input = this.getContentsBeforeTransformation(); |
|
cacheDereferencedElement(input); |
|
XMLSignatureInput output = input; |
|
Transforms transforms = this.getTransforms(); |
|
|
|
if (transforms != null) { |
|
for (int i = 0; i < transforms.getLength(); i++) { |
|
Transform t = transforms.item(i); |
|
String uri = t.getURI(); |
|
|
|
if (uri.equals(Transforms.TRANSFORM_C14N_EXCL_OMIT_COMMENTS) |
|
|| uri.equals(Transforms.TRANSFORM_C14N_EXCL_WITH_COMMENTS) |
|
|| uri.equals(Transforms.TRANSFORM_C14N_OMIT_COMMENTS) |
|
|| uri.equals(Transforms.TRANSFORM_C14N_WITH_COMMENTS) |
|
|| uri.equals(Transforms.TRANSFORM_C14N11_OMIT_COMMENTS) |
|
|| uri.equals(Transforms.TRANSFORM_C14N11_WITH_COMMENTS)) { |
|
break; |
|
} |
|
|
|
output = t.performTransform(output, null); |
|
} |
|
|
|
output.setSourceURI(input.getSourceURI()); |
|
} |
|
return output; |
|
} catch (IOException ex) { |
|
throw new XMLSignatureException(ex); |
|
} catch (ResourceResolverException ex) { |
|
throw new XMLSignatureException(ex); |
|
} catch (CanonicalizationException ex) { |
|
throw new XMLSignatureException(ex); |
|
} catch (InvalidCanonicalizerException ex) { |
|
throw new XMLSignatureException(ex); |
|
} catch (TransformationException ex) { |
|
throw new XMLSignatureException(ex); |
|
} catch (XMLSecurityException ex) { |
|
throw new XMLSignatureException(ex); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public String getHTMLRepresentation() throws XMLSignatureException { |
|
try { |
|
XMLSignatureInput nodes = this.getNodesetBeforeFirstCanonicalization(); |
|
|
|
Transforms transforms = this.getTransforms(); |
|
Transform c14nTransform = null; |
|
|
|
if (transforms != null) { |
|
for (int i = 0; i < transforms.getLength(); i++) { |
|
Transform t = transforms.item(i); |
|
String uri = t.getURI(); |
|
|
|
if (uri.equals(Transforms.TRANSFORM_C14N_EXCL_OMIT_COMMENTS) |
|
|| uri.equals(Transforms.TRANSFORM_C14N_EXCL_WITH_COMMENTS)) { |
|
c14nTransform = t; |
|
break; |
|
} |
|
} |
|
} |
|
|
|
Set<String> inclusiveNamespaces = new HashSet<>(); |
|
if (c14nTransform != null |
|
&& c14nTransform.length( |
|
InclusiveNamespaces.ExclusiveCanonicalizationNamespace, |
|
InclusiveNamespaces._TAG_EC_INCLUSIVENAMESPACES) == 1) { |
|
|
|
|
|
InclusiveNamespaces in = |
|
new InclusiveNamespaces( |
|
XMLUtils.selectNode( |
|
c14nTransform.getElement().getFirstChild(), |
|
InclusiveNamespaces.ExclusiveCanonicalizationNamespace, |
|
InclusiveNamespaces._TAG_EC_INCLUSIVENAMESPACES, |
|
0 |
|
), this.getBaseURI()); |
|
|
|
inclusiveNamespaces = |
|
InclusiveNamespaces.prefixStr2Set(in.getInclusiveNamespaces()); |
|
} |
|
|
|
return nodes.getHTMLRepresentation(inclusiveNamespaces); |
|
} catch (TransformationException ex) { |
|
throw new XMLSignatureException(ex); |
|
} catch (InvalidTransformException ex) { |
|
throw new XMLSignatureException(ex); |
|
} catch (XMLSecurityException ex) { |
|
throw new XMLSignatureException(ex); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
public XMLSignatureInput getTransformsOutput() { |
|
return this.transformsOutput; |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
public ReferenceData getReferenceData() { |
|
return referenceData; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
protected XMLSignatureInput dereferenceURIandPerformTransforms(OutputStream os) |
|
throws XMLSignatureException { |
|
try { |
|
XMLSignatureInput input = this.getContentsBeforeTransformation(); |
|
cacheDereferencedElement(input); |
|
|
|
XMLSignatureInput output = this.getContentsAfterTransformation(input, os); |
|
this.transformsOutput = output; |
|
return output; |
|
} catch (XMLSecurityException ex) { |
|
throw new ReferenceNotInitializedException(ex); |
|
} |
|
} |
|
|
|
|
|
|
|
*/ |
|
private void cacheDereferencedElement(XMLSignatureInput input) { |
|
if (input.isNodeSet()) { |
|
try { |
|
final Set<Node> s = input.getNodeSet(); |
|
referenceData = new ReferenceNodeSetData() { |
|
public Iterator<Node> iterator() { |
|
return new Iterator<Node>() { |
|
|
|
Iterator<Node> sIterator = s.iterator(); |
|
|
|
@Override |
|
public boolean hasNext() { |
|
return sIterator.hasNext(); |
|
} |
|
|
|
@Override |
|
public Node next() { |
|
return sIterator.next(); |
|
} |
|
|
|
@Override |
|
public void remove() { |
|
throw new UnsupportedOperationException(); |
|
} |
|
}; |
|
} |
|
}; |
|
} catch (Exception e) { |
|
|
|
LOG.warn("cannot cache dereferenced data: " + e); |
|
} |
|
} else if (input.isElement()) { |
|
referenceData = new ReferenceSubTreeData |
|
(input.getSubNode(), input.isExcludeComments()); |
|
} else if (input.isOctetStream() || input.isByteArray()) { |
|
try { |
|
referenceData = new ReferenceOctetStreamData |
|
(input.getOctetStream(), input.getSourceURI(), |
|
input.getMIMEType()); |
|
} catch (IOException ioe) { |
|
|
|
LOG.warn("cannot cache dereferenced data: " + ioe); |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public Transforms getTransforms() |
|
throws XMLSignatureException, InvalidTransformException, |
|
TransformationException, XMLSecurityException { |
|
return transforms; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public byte[] getReferencedBytes() |
|
throws ReferenceNotInitializedException, XMLSignatureException { |
|
try { |
|
XMLSignatureInput output = this.dereferenceURIandPerformTransforms(null); |
|
return output.getBytes(); |
|
} catch (IOException ex) { |
|
throw new ReferenceNotInitializedException(ex); |
|
} catch (CanonicalizationException ex) { |
|
throw new ReferenceNotInitializedException(ex); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private byte[] calculateDigest(boolean validating) |
|
throws ReferenceNotInitializedException, XMLSignatureException { |
|
XMLSignatureInput input = this.getContentsBeforeTransformation(); |
|
if (input.isPreCalculatedDigest()) { |
|
return getPreCalculatedDigest(input); |
|
} |
|
|
|
MessageDigestAlgorithm mda = this.getMessageDigestAlgorithm(); |
|
mda.reset(); |
|
|
|
try (DigesterOutputStream diOs = new DigesterOutputStream(mda); |
|
OutputStream os = new UnsyncBufferedOutputStream(diOs)) { |
|
XMLSignatureInput output = this.dereferenceURIandPerformTransforms(os); |
|
// if signing and c14n11 property == true explicitly add |
|
|
|
if (Reference.useC14N11 && !validating && !output.isOutputStreamSet() |
|
&& !output.isOctetStream()) { |
|
if (transforms == null) { |
|
transforms = new Transforms(getDocument()); |
|
transforms.setSecureValidation(secureValidation); |
|
getElement().insertBefore(transforms.getElement(), digestMethodElem); |
|
} |
|
transforms.addTransform(Transforms.TRANSFORM_C14N11_OMIT_COMMENTS); |
|
output.updateOutputStream(os, true); |
|
} else { |
|
output.updateOutputStream(os); |
|
} |
|
os.flush(); |
|
|
|
if (output.getOctetStreamReal() != null) { |
|
output.getOctetStreamReal().close(); |
|
} |
|
|
|
//this.getReferencedBytes(diOs); |
|
//mda.update(data); |
|
|
|
return diOs.getDigestValue(); |
|
} catch (XMLSecurityException ex) { |
|
throw new ReferenceNotInitializedException(ex); |
|
} catch (IOException ex) { |
|
throw new ReferenceNotInitializedException(ex); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private byte[] getPreCalculatedDigest(XMLSignatureInput input) |
|
throws ReferenceNotInitializedException { |
|
LOG.debug("Verifying element with pre-calculated digest"); |
|
String preCalculatedDigest = input.getPreCalculatedDigest(); |
|
return Base64.getMimeDecoder().decode(preCalculatedDigest); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public byte[] getDigestValue() throws XMLSecurityException { |
|
if (digestValueElement == null) { |
|
|
|
Object[] exArgs ={ Constants._TAG_DIGESTVALUE, Constants.SignatureSpecNS }; |
|
throw new XMLSecurityException( |
|
"signature.Verification.NoSignatureElement", exArgs |
|
); |
|
} |
|
String content = XMLUtils.getFullTextChildrenFromElement(digestValueElement); |
|
return Base64.getMimeDecoder().decode(content); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public boolean verify() |
|
throws ReferenceNotInitializedException, XMLSecurityException { |
|
byte[] elemDig = this.getDigestValue(); |
|
byte[] calcDig = this.calculateDigest(true); |
|
boolean equal = MessageDigestAlgorithm.isEqual(elemDig, calcDig); |
|
|
|
if (!equal) { |
|
LOG.warn("Verification failed for URI \"" + this.getURI() + "\""); |
|
LOG.warn("Expected Digest: " + Base64.getMimeEncoder().encodeToString(elemDig)); |
|
LOG.warn("Actual Digest: " + Base64.getMimeEncoder().encodeToString(calcDig)); |
|
} else { |
|
LOG.debug("Verification successful for URI \"{}\"", this.getURI()); |
|
} |
|
|
|
return equal; |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
public String getBaseLocalName() { |
|
return Constants._TAG_REFERENCE; |
|
} |
|
} |