Refactored, moved functions from utils. Makes for a better fit with the model

This commit is contained in:
Roland Hedberg
2010-03-16 11:03:52 +01:00
parent 6f2160a6da
commit 86bbc70fce
12 changed files with 221 additions and 182 deletions

View File

@@ -323,6 +323,60 @@ class ExtensionContainer(object):
return results
def make_vals(val, klass, klass_inst=None, prop=None, part=False,
base64encode=False):
"""
Creates a class instance with a specified value, the specified
class instance may be a value on a property in a defined class instance.
:param val: The value
:param klass: The value class
:param klass_inst: The class instance which has a property on which
what this function returns is a value.
:param prop: The property which the value should be assigned to.
:param part: If the value is one of a possible list of values it should be
handled slightly different compared to if it isn't.
:return: Value class instance
"""
cinst = None
print "make_vals(%s, %s)" % (val, klass)
if isinstance(val, dict):
print "+"
cinst = klass().loadd(val, base64encode=base64encode)
else:
print "++"
try:
cinst = klass().set_text(val)
except ValueError, excp:
print "!! %s" % (excp,)
if not part:
cis = [make_vals(sval, klass, klass_inst, prop, True,
base64encode) for sval in val]
setattr(klass_inst, prop, cis)
else:
raise
print "CINST: %s, part: %s" % (cinst,part)
if part:
return cinst
else:
if cinst:
cis = [cinst]
setattr(klass_inst, prop, cis)
def make_instance(klass, spec, base64encode=False):
"""
Constructs a class instance containing the specified information
:param klass: The class
:param spec: Information to be placed in the instance (a dictionary)
:return: The instance
"""
return klass().loadd(spec, base64encode)
class SamlBase(ExtensionContainer):
"""A foundation class on which SAML classes are built. It
@@ -462,6 +516,54 @@ class SamlBase(ExtensionContainer):
childs.append(member)
return childs
def set_text(self, val, base64encode=False):
""" """
print "set_text: %s" % (val,)
if isinstance(val, bool):
if val:
setattr(self, "text", "true")
else:
setattr(self, "text", "false")
elif isinstance(val, int):
setattr(self, "text", "%d" % val)
elif isinstance(val, basestring):
setattr(self, "text", val)
elif val == None:
pass
else:
raise ValueError( "Type it shouldn't be '%s'" % (val,))
return self
def loadd(self, ava, base64encode=False):
""" """
for prop in self.c_attributes.values():
print "# %s" % (prop)
if prop in ava:
if isinstance(ava[prop], bool):
setattr(self, prop, "%s" % ava[prop])
elif isinstance(ava[prop], int):
setattr(self, prop, "%d" % ava[prop])
else:
setattr(self, prop, ava[prop])
if "text" in ava:
self.set_text(ava["text"], base64encode)
for prop, klassdef in self.c_children.values():
print "## %s, %s" % (prop, klassdef)
if prop in ava:
print "### %s" % ava[prop]
if isinstance(klassdef, list): # means there can be a list of values
make_vals(ava[prop], klassdef[0], self, prop,
base64encode=base64encode)
else:
cis = make_vals(ava[prop], klassdef, self, prop, True,
base64encode)
setattr(self, prop, cis)
return self
def extension_element_to_element(extension_element, translation_function,
namespace=None):
""" """

View File

@@ -18,6 +18,7 @@
"""Contains classes and functions that a SAML2.0 Service Provider (SP) may use
to conclude its tasks.
"""
import os
import urllib
import saml2
@@ -25,11 +26,11 @@ import base64
import time
import sys
from saml2.time_util import str_to_time, instant
from saml2.utils import sid, deflate_and_base64_encode, make_instance
from saml2.utils import sid, deflate_and_base64_encode
from saml2.utils import do_attributes, args2dict
from saml2 import samlp, saml, extension_element_to_element
from saml2 import VERSION, class_name
from saml2 import VERSION, class_name, make_instance
from saml2.sigver import correctly_signed_response, decrypt
from saml2.sigver import pre_signature_part, sign_assertion_using_xmlsec
from saml2.sigver import sign_statement_using_xmlsec

View File

@@ -854,7 +854,7 @@ def _decode_attribute_value(typ, text):
if typ == XSD + "float" or typ == XSD + "double":
return str(float(text))
if typ == XSD + "boolean":
return "%s" % (text == "true")
return "%s" % (text == "true" or text == "True")
if typ == XSD + "base64Binary":
import base64
return base64.decodestring(text)
@@ -881,7 +881,36 @@ class AttributeValue(SamlBase):
self.text = _decode_attribute_value(typ, tree.text)
else:
self.text = tree.text
def set_text(self, val, base64encode=False):
print "AV.set_text(%s)" % (val,)
if base64encode:
import base64
val = base64.encodestring(val)
setattr(self, "type", "xs:base64Binary")
else:
if isinstance(val, basestring):
setattr(self, "type", "xs:string")
elif isinstance(val, bool):
if val:
val = "true"
else:
val = "false"
setattr(self, "type", "xs:boolean")
elif isinstance(val, int):
val = str(val)
setattr(self, "type", "xs:integer")
elif isinstance(val, float):
val = str(val)
setattr(self, "type", "xs:float")
elif val == None:
val = ""
else:
raise ValueError
setattr(self, "text", val)
return self
def attribute_value_from_string(xml_string):
""" Create AttributeValue instance from an XML string """
return saml2.create_class_from_xml_string(AttributeValue, xml_string)

View File

@@ -21,9 +21,9 @@ or attribute authority (AA) may use to conclude its tasks.
import shelve
from saml2 import saml, samlp, VERSION
from saml2 import saml, samlp, VERSION, make_instance
from saml2.utils import sid, decode_base64_and_inflate, make_instance
from saml2.utils import sid, decode_base64_and_inflate
from saml2.utils import response_factory, do_ava_statement
from saml2.utils import MissingValue, args2dict
from saml2.utils import success_status_factory, assertion_factory

View File

@@ -98,112 +98,6 @@ def sid(seed=""):
ident.update(seed)
return ident.hexdigest()
def make_vals(val, klass, klass_inst=None, prop=None, part=False,
base64encode=False):
"""
Creates a class instance with a specified value, the specified
class instance are a value on a property in a defined class instance.
:param val: The value
:param klass: The value class
:param klass_inst: The class instance which has a property on which
what this function returns is a value.
:param prop: The property which the value should be assigned to.
:param part: If the value is one of a possible list of values it should be
handled slightly different compared to if it isn't.
:return: Value class instance
"""
cinst = None
#print "_make_val: %s %s (%s) [%s]" % (prop,val,klass,part)
if isinstance(val, bool):
cinst = klass(text="%s" % val)
elif isinstance(val, int):
cinst = klass(text="%d" % val)
elif isinstance(val, basestring):
cinst = klass(text=val)
elif val == None:
cinst = klass()
elif isinstance(val, dict):
cinst = make_instance(klass, val, base64encode=base64encode)
elif not part:
cis = [make_vals(sval, klass, klass_inst, prop, True,
base64encode) for sval in val]
setattr(klass_inst, prop, cis)
else:
raise ValueError("strange instance type: %s on %s" % (type(val), val))
if part:
return cinst
else:
if cinst:
cis = [cinst]
setattr(klass_inst, prop, cis)
def make_instance(klass, spec, base64encode=False):
"""
Constructs a class instance containing the specified information
:param klass: The class
:param spec: Information to be placed in the instance (a dictionary)
:return: The instance
"""
#print "----- %s -----" % klass
#print "..... %s ....." % spec
klass_inst = klass()
for prop in klass.c_attributes.values():
#print "# %s" % (prop)
if prop in spec:
if isinstance(spec[prop], bool):
setattr(klass_inst, prop,"%s" % spec[prop])
elif isinstance(spec[prop], int):
setattr(klass_inst, prop, "%d" % spec[prop])
else:
setattr(klass_inst, prop, spec[prop])
if "text" in spec:
val = spec["text"]
print "<<", klass
if klass == saml.AttributeValue:
print ">> AVA"
if base64encode:
import base64
val = base64.encodestring(val)
setattr(klass_inst, "type", "xs:base64Binary")
else:
if isinstance(val, basestring):
print "basestring"
setattr(klass_inst, "type", "xs:string")
print klass_inst.__dict__
elif isinstance(val, bool):
print "boolean", val
if val:
val = "true"
else:
val = "false"
setattr(klass_inst, "type", "xs:boolean")
elif isinstance(val, int):
val = str(val)
setattr(klass_inst, "type", "xs:integer")
elif isinstance(val, float):
val = str(val)
setattr(klass_inst, "type", "xs:float")
setattr(klass_inst, "text", val)
for prop, klass in klass.c_children.values():
#print "## %s, %s" % (prop, klass)
if prop in spec:
#print "%s" % spec[prop]
if isinstance(klass, list): # means there can be a list of values
make_vals(spec[prop], klass[0], klass_inst, prop,
base64encode=base64encode)
else:
cis = make_vals(spec[prop], klass, klass_inst, prop, True,
base64encode)
setattr(klass_inst, prop, cis)
#+print ">>> %s <<<" % klass_inst
return klass_inst
def parse_attribute_map(filenames):
"""
Expects a file with each line being composed of the oid for the attribute

View File

@@ -1,9 +1,10 @@
#!/usr/bin/env python
from saml2 import create_class_from_xml_string, class_name
from saml2 import create_class_from_xml_string, class_name, make_vals, md
from saml2.saml import NameID, Issuer, SubjectLocality, AuthnContextClassRef
from saml2.saml import SubjectConfirmationData, SubjectConfirmation
import saml2
from py.test import raises
try:
from xml.etree import cElementTree as ElementTree
@@ -365,4 +366,32 @@ def test_to_fro_string_1():
assert klee.namespace == cpyee.namespace
def test_make_vals_str():
kl = make_vals("Jeter",md.GivenName, part=True)
assert isinstance(kl, md.GivenName)
assert kl.text == "Jeter"
def test_make_vals_int():
kl = make_vals(1024,md.KeySize, part=True)
assert isinstance(kl, md.KeySize)
assert kl.text == "1024"
def test_exception_make_vals_int_not_part():
raises(TypeError, "make_vals(1024,md.KeySize)")
raises(TypeError, "make_vals(1024,md.KeySize,md.EncryptionMethod())")
raises(AttributeError, "make_vals(1024,md.KeySize,prop='key_size')")
def test_make_vals_list_of_ints():
em = md.EncryptionMethod()
make_vals([1024,2048], md.KeySize, em, "key_size")
assert len(em.key_size) == 2
def test_make_vals_list_of_strs():
cp = md.ContactPerson()
make_vals(["Derek","Sanderson"], md.GivenName, cp, "given_name")
assert len(cp.given_name) == 2
assert _eq([i.text for i in cp.given_name],["Sanderson","Derek"])
def test_exception_make_vals_value_error():
raises(ValueError, "make_vals((1024,'xyz'), md.KeySize, part=True)")

View File

@@ -5,8 +5,8 @@ import zlib
import base64
import gzip
from saml2 import utils, saml, samlp, md
from saml2.utils import do_attribute_statement, make_instance
from saml2 import utils, saml, samlp, md, make_instance
from saml2.utils import do_attribute_statement
from saml2.sigver import make_temp
from saml2.config import do_assertions
from saml2.saml import Attribute, NAME_FORMAT_URI, AttributeValue
@@ -78,39 +78,11 @@ def test_status_from_exception():
print status_text
assert status_text == ERROR_STATUS
def test_make_vals_str():
kl = utils.make_vals("Jeter",md.GivenName, part=True)
assert isinstance(kl, md.GivenName)
assert kl.text == "Jeter"
def test_make_vals_int():
kl = utils.make_vals(1024,md.KeySize, part=True)
assert isinstance(kl, md.KeySize)
assert kl.text == "1024"
def test_exception_make_vals_int_not_part():
raises(TypeError, "utils.make_vals(1024,md.KeySize)")
raises(TypeError, "utils.make_vals(1024,md.KeySize,md.EncryptionMethod())")
raises(AttributeError, "utils.make_vals(1024,md.KeySize,prop='key_size')")
def test_make_vals_list_of_ints():
em = md.EncryptionMethod()
utils.make_vals([1024,2048], md.KeySize, em, "key_size")
assert len(em.key_size) == 2
def test_make_vals_list_of_strs():
cp = md.ContactPerson()
utils.make_vals(["Derek","Sanderson"], md.GivenName, cp, "given_name")
assert len(cp.given_name) == 2
assert _eq([i.text for i in cp.given_name],["Sanderson","Derek"])
def test_exception_make_vals_value_error():
raises(ValueError, "utils.make_vals((1024,'xyz'), md.KeySize, part=True)")
def test_attribute_sn():
attr = utils.do_attributes({"surName":"Jeter"})
assert len(attr) == 1
print attr
inst = make_instance(saml.Attribute, attr[0])
print inst
assert inst.name == "surName"

View File

@@ -1,6 +1,6 @@
import os
from saml2 import metadata, utils
from saml2 import metadata, utils, make_vals, make_instance
from saml2 import NAMESPACE as SAML2_NAMESPACE
from saml2 import BINDING_SOAP
from saml2 import md, saml, samlp
@@ -132,46 +132,46 @@ def test_sp_metadata():
def test_construct_organisation_name():
o = md.Organization()
utils.make_vals({"text":"Exempel AB", "lang":"se"},
make_vals({"text":"Exempel AB", "lang":"se"},
md.OrganizationName, o, "organization_name")
print o
assert str(o) == """<?xml version='1.0' encoding='UTF-8'?>
<ns0:Organization xmlns:ns0="urn:oasis:names:tc:SAML:2.0:metadata"><ns0:OrganizationName ns1:lang="se" xmlns:ns1="http:#www.w3.org/XML/1998/namespace">Exempel AB</ns0:OrganizationName></ns0:Organization>"""
def test_make_int_value():
val = utils.make_vals( 1, saml.AttributeValue, part=True)
val = make_vals( 1, saml.AttributeValue, part=True)
assert isinstance(val, saml.AttributeValue)
assert val.text == "1"
def test_make_true_value():
val = utils.make_vals( True, saml.AttributeValue, part=True )
val = make_vals( True, saml.AttributeValue, part=True )
assert isinstance(val, saml.AttributeValue)
assert val.text == "True"
assert val.text == "true"
def test_make_false_value():
val = utils.make_vals( False, saml.AttributeValue, part=True )
val = make_vals( False, saml.AttributeValue, part=True )
assert isinstance(val, saml.AttributeValue)
assert val.text == "False"
assert val.text == "false"
NO_VALUE = """<?xml version='1.0' encoding='UTF-8'?>
<ns0:AttributeValue xmlns:ns0="urn:oasis:names:tc:SAML:2.0:assertion" />"""
def test_make_no_value():
val = utils.make_vals( None, saml.AttributeValue, part=True )
val = make_vals( None, saml.AttributeValue, part=True )
assert isinstance(val, saml.AttributeValue)
assert val.text == None
assert val.text == ""
print val
assert "%s" % val == NO_VALUE
def test_make_string():
val = utils.make_vals( "example", saml.AttributeValue, part=True )
val = make_vals( "example", saml.AttributeValue, part=True )
assert isinstance(val, saml.AttributeValue)
assert val.text == "example"
def test_make_list_of_strings():
attr = saml.Attribute()
vals = ["foo", "bar"]
val = utils.make_vals(vals, saml.AttributeValue, attr,
val = make_vals(vals, saml.AttributeValue, attr,
"attribute_value")
assert attr.keyswv() == ["attribute_value"]
print attr.attribute_value
@@ -180,14 +180,14 @@ def test_make_list_of_strings():
def test_make_dict():
vals = ["foo", "bar"]
attrval = { "attribute_value": vals}
attr = utils.make_vals(attrval, saml.Attribute, part=True)
attr = make_vals(attrval, saml.Attribute, part=True)
assert attr.keyswv() == ["attribute_value"]
assert _eq([val.text for val in attr.attribute_value], vals)
# ------------ Constructing metadata ----------------------------------------
def test_construct_contact():
c = utils.make_instance(md.ContactPerson, {
c = make_instance(md.ContactPerson, {
"given_name":"Roland",
"sur_name": "Hedberg",
"email_address": "roland@catalogix.se",
@@ -200,7 +200,7 @@ def test_construct_contact():
def test_construct_organisation():
c = utils.make_instance( md.Organization, {
c = make_instance( md.Organization, {
"organization_name": ["Example Co.",
{"text":"Exempel AB", "lang":"se"}],
"organization_url": "http://www.example.com/"
@@ -213,7 +213,7 @@ def test_construct_organisation():
assert len(c.organization_url) == 1
def test_construct_entity_descr_1():
ed = utils.make_instance(md.EntityDescriptor,
ed = make_instance(md.EntityDescriptor,
{"organization": {
"organization_name":"Catalogix",
"organization_url": "http://www.catalogix.se/"},
@@ -228,7 +228,7 @@ def test_construct_entity_descr_1():
assert org.organization_url[0].text == "http://www.catalogix.se/"
def test_construct_entity_descr_2():
ed = utils.make_instance(md.EntityDescriptor,
ed = make_instance(md.EntityDescriptor,
{"organization": {
"organization_name":"Catalogix",
"organization_url": "http://www.catalogix.se/"},
@@ -264,7 +264,7 @@ def test_construct_key_descriptor():
}
}
}
kd = utils.make_instance(md.KeyDescriptor, spec)
kd = make_instance(md.KeyDescriptor, spec)
assert _eq(kd.keyswv(), ["use", "key_info"])
assert kd.use == "signing"
ki = kd.key_info
@@ -286,7 +286,7 @@ def test_construct_key_descriptor_with_key_name():
}
}
}
kd = utils.make_instance(md.KeyDescriptor, spec)
kd = make_instance(md.KeyDescriptor, spec)
assert _eq(kd.keyswv(), ["use", "key_info"])
assert kd.use == "signing"
ki = kd.key_info
@@ -300,7 +300,7 @@ def test_construct_key_descriptor_with_key_name():
assert len(data.x509_certificate[0].text.strip()) == len(cert)
def test_construct_AttributeAuthorityDescriptor():
aad = utils.make_instance(
aad = make_instance(
md.AttributeAuthorityDescriptor, {
"valid_until": time_util.in_a_while(30), # 30 days from now
"id": "aad.example.com",
@@ -354,6 +354,6 @@ def test_status():
},
"status_message": "Error resolving principal",
}
status_text = "%s" % utils.make_instance( samlp.Status, input)
status_text = "%s" % make_instance( samlp.Status, input)
assert status_text == STATUS_RESULT

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python
from saml2 import sigver
from saml2 import sigver, make_instance
from saml2 import utils
from saml2 import time_util
from saml2 import saml
@@ -32,14 +32,14 @@ def test_non_verify_2(xmlsec):
raises(sigver.SignatureError,sigver.correctly_signed_response,
xml_response, xmlsec)
SIGNED_VALUE= """Y88SEXrU3emeoaTgEqUKYAvDtWiLpPMx1sClw0GJV98O6A5QRvB14vNs8xnXNFFZ
XVjksKECcqmf10k/2C3oJfaEOaM4w0DgVLXeuJU08irXfdHcoe1g3276F1If1Kh7
63F7ihzh2ZeWV9OOO8tXofR9GCLIpPECbK+3/D4eEDY="""
SIGNED_VALUE= """AS1kHHtA4eTOU2XLTWhLMSJQ6V+TSDymRoTF78CqjrYURNLk9wjdPjAReNn9eykv
ryFiHNk0p9wMBknha5pH8aeCI/LmcVhLa5xteGZrtE/Udh5vv8z4kRQX51Uz/5x8
ToiobGw83MEW6A0dRUn0O20NBMMTaFZZPXye7RvVlHY="""
DIGEST_VALUE = "9cQ0c72QfbQr1KkH9MCwL5Wm1EQ="
DIGEST_VALUE = "WFRXmImfoO3M6JOLE6BGGpU9Ud0="
def test_sign(xmlsec):
ass = utils.make_instance(saml.Assertion, {
ass = make_instance(saml.Assertion, {
"version": "2.0",
"id": "11111",
"issue_instant": "2009-10-30T13:20:28Z",

View File

@@ -2,9 +2,9 @@
# -*- coding: utf-8 -*-
from saml2.server import Server
from saml2 import server
from saml2 import server, make_instance
from saml2 import samlp, saml, client, utils
from saml2.utils import make_instance, OtherError
from saml2.utils import OtherError
from saml2.utils import do_attribute_statement
from py.test import raises
import shelve
@@ -77,12 +77,12 @@ class TestServer1():
assertion=utils.assertion_factory(
subject = utils.args2dict("_aaa",
name_id=saml.NAMEID_FORMAT_TRANSIENT),
attribute_statement = utils.args2dict([
attribute_statement = [
utils.args2dict(attribute_value="Derek",
friendly_name="givenName"),
utils.args2dict(attribute_value="Jeter",
friendly_name="surName"),
]),
],
issuer=self.server.issuer(),
),
issuer=self.server.issuer(),
@@ -132,7 +132,7 @@ class TestServer1():
status = None
except OtherError, oe:
print oe.args
status = utils.make_instance(samlp.Status,
status = make_instance(samlp.Status,
utils.status_from_exception_factory(oe))
assert status
@@ -188,6 +188,18 @@ class TestServer1():
assert len(assertion.authn_statement) == 1
assert assertion.conditions
assert len(assertion.attribute_statement) == 1
attribute_statement = assertion.attribute_statement[0]
print attribute_statement
#<ns0:AttributeStatement xmlns:ns0="urn:oasis:names:tc:SAML:2.0:assertion"><ns0:Attribute FriendlyName="eduPersonEntitlement" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.7" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><ns0:AttributeValue ns1:type="xs:string" xmlns:ns1="http://www.w3.org/2001/XMLSchema-instance">Bat</ns0:AttributeValue></ns0:Attribute></ns0:AttributeStatement>
assert len(attribute_statement.attribute) == 1
attribute = attribute_statement.attribute[0]
assert len(attribute.attribute_value) == 1
assert attribute.friendly_name == "eduPersonEntitlement"
assert attribute.name == "urn:oid:1.3.6.1.4.1.5923.1.1.1.7"
assert attribute.name_format == "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
value = attribute.attribute_value[0]
assert value.text.strip() == "Bat"
assert value.type == "xs:string"
assert assertion.subject
assert assertion.subject.name_id
assert len(assertion.subject.subject_confirmation) == 1
@@ -340,7 +352,7 @@ class TestServer1():
resp_str = self.server.authn_response(ava,
"1", "http://local:8087/",
"urn:mace:example.com:saml:roland:sp",
utils.make_instance(samlp.NameIDPolicy,
make_instance(samlp.NameIDPolicy,
utils.args2dict(
format=saml.NAMEID_FORMAT_TRANSIENT,
allow_create="true")),

View File

@@ -3,7 +3,7 @@
from saml2.client import Saml2Client
from saml2 import samlp, client, BINDING_HTTP_POST
from saml2 import saml, utils, config, class_name
from saml2 import saml, utils, config, class_name, make_instance
from saml2.sigver import correctly_signed_authn_request, verify_signature
from saml2.sigver import correctly_signed_response
from saml2.server import Server
@@ -206,7 +206,7 @@ class TestClient:
assert req == None
def test_idp_entry(self):
idp_entry = utils.make_instance( samlp.IDPEntry,
idp_entry = make_instance( samlp.IDPEntry,
self.client.idp_entry(name="Umeå Universitet",
location="https://idp.umu.se/"))
@@ -214,7 +214,7 @@ class TestClient:
assert idp_entry.loc == "https://idp.umu.se/"
def test_scope(self):
scope = utils.make_instance(samlp.Scoping, self.client.scoping(
scope = make_instance(samlp.Scoping, self.client.scoping(
[self.client.idp_entry(name="Umeå Universitet",
location="https://idp.umu.se/")]))

View File

@@ -2,7 +2,7 @@
import os
import getopt
from saml2 import utils, md, samlp, BINDING_HTTP_POST, BINDING_HTTP_REDIRECT
from saml2 import BINDING_SOAP, class_name
from saml2 import BINDING_SOAP, class_name, make_instance
from saml2.time_util import in_a_while
from saml2.utils import parse_attribute_map
from saml2.saml import NAME_FORMAT_URI
@@ -213,7 +213,7 @@ def entities_descriptor(eds, valid_for, name, id, sign, xmlsec, keyfile):
if sign:
d["signature"] = pre_signature_part(d["id"])
statement = utils.make_instance(md.EntitiesDescriptor, d)
statement = make_instance(md.EntitiesDescriptor, d)
if sign:
statement = sign_statement_using_xmlsec("%s" % statement,
class_name(statement),