This is an automated email from the ASF dual-hosted git repository.

smolnar pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ambari.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 66a3bf9  AMBARI-24861. New wrapper class on Python side for 
LDAP-related data for use in service advisors (#2579)
66a3bf9 is described below

commit 66a3bf93adbecdda74b1ea76cde4f39c49b9beaa
Author: Sandor Molnar <[email protected]>
AuthorDate: Tue Nov 6 19:08:46 2018 +0100

    AMBARI-24861. New wrapper class on Python side for LDAP-related data for 
use in service advisors (#2579)
---
 .../ldap/domain/AmbariLdapConfiguration.java       |   4 +
 .../main/resources/stacks/ambari_configuration.py  | 225 +++++++++++++++++++++
 ...configuration.py => TestAmbariConfiguration.py} | 126 +++++++++++-
 3 files changed, 354 insertions(+), 1 deletion(-)

diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/ldap/domain/AmbariLdapConfiguration.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/ldap/domain/AmbariLdapConfiguration.java
index fe99a35..c55f337 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/ldap/domain/AmbariLdapConfiguration.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/ldap/domain/AmbariLdapConfiguration.java
@@ -34,6 +34,10 @@ import org.slf4j.LoggerFactory;
 /**
  * This class is an immutable representation of all the LDAP related
  * configurationMap entries.
+ * <p>
+ * <strong>IMPORTANT: </strong>in case you declare any new LDAP related 
property
+ * please do it in the Python class
+ * <code>stacks.ambari_configuration.AmbariLDAPConfiguration</code> too.
  */
 public class AmbariLdapConfiguration {
 
diff --git a/ambari-server/src/main/resources/stacks/ambari_configuration.py 
b/ambari-server/src/main/resources/stacks/ambari_configuration.py
index d327c44..9104c32 100644
--- a/ambari-server/src/main/resources/stacks/ambari_configuration.py
+++ b/ambari-server/src/main/resources/stacks/ambari_configuration.py
@@ -107,6 +107,19 @@ class AmbariConfiguration(object):
     """
     return AmbariSSODetails(self.get_ambari_sso_configuration())
 
+  def get_ambari_ldap_configuration(self):
+    """
+    Safely gets a dictionary of properties for the "ldap-configuration" 
category.
+
+    :return: a dictionary or None, if "ldap-configuration" is not available
+    """
+    return self.get_ambari_server_configuration_category("ldap-configuration")
+
+  def get_ambari_ldap_details(self):
+    """
+    :return: instance of AmbariLDAPConfiguration that may be used to configure 
a service for LDAP integration
+    """
+    return AmbariLDAPConfiguration(self.get_ambari_ldap_configuration())
 
 class AmbariSSODetails(object):
   """
@@ -262,3 +275,215 @@ class AmbariSSODetails(object):
         public_cert = public_cert.replace('\n', '')
 
     return public_cert
+
+
+class AmbariLDAPConfiguration:
+  """
+  AmbariLDAPConfiguration encapsulates the LDAP configuration data specified 
in the ambari-server-configuration data.
+  The public API of class mirrors the following Java class's public API 
(except for trust store related API and getLdapServerProperties which we do not 
need in Pyton side):
+  org.apache.ambari.server.ldap.domain.AmbariLdapConfiguration
+  """
+
+  def __init__(self, ldap_properties):
+    self.ldap_properties = ldap_properties
+
+  def is_ldap_enabled(self):
+    return "true" == _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.authentication.enabled')
+
+  def get_server_host(self):
+    '''
+    :return: The LDAP URL host used for connecting to an LDAP server when 
authenticating users or None if 
ldap-configuration/ambari.ldap.connectivity.server.host is not specified
+    '''
+    return _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.connectivity.server.host')
+
+  def get_server_port(self):
+    '''
+    :return: The LDAP URL port (as an integer) used for connecting to an LDAP 
server when authenticating users or None if 
ldap-configuration/ambari.ldap.connectivity.server.port is not specified
+    '''
+    ldap_server_port = _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.connectivity.server.port')
+    return int(ldap_server_port) if ldap_server_port is not None else None
+
+  def get_server_url(self):
+    '''
+    :return: The LDAP URL (host:port) used for connecting to an LDAP server 
when authenticating users
+    '''
+    ldap_host = self.get_server_host()
+    ldap_port = self.get_server_port()
+    return None if ldap_host is None or ldap_port is None else 
'{}:{}'.format(ldap_host,ldap_port)
+
+  def get_secondary_server_host(self):
+    '''
+    :return: A second LDAP URL host to use as a backup when authenticating 
users or None if 
ldap-configuration/ambari.ldap.connectivity.secondary.server.host is not 
specified
+    '''
+    return _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.connectivity.secondary.server.host')
+
+  def get_secondary_server_port(self):
+    '''
+    :return: A second LDAP URL port (as an integer) to use as a backup when 
authenticating users or None if 
ldap-configuration/ambari.ldap.connectivity.secondary.server.port is not 
specified
+    '''
+    ldap_server_secondary_port = _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.connectivity.secondary.server.port')
+    return int(ldap_server_secondary_port) if ldap_server_secondary_port is 
not None else None
+
+  def get_secondary_server_url(self):
+    '''
+    :return: A second LDAP URL (host:port) used for connecting to an LDAP 
server when authenticating users
+    '''
+    ldap_host = self.get_secondary_server_host()
+    ldap_port = self.get_secondary_server_port()
+    return None if ldap_host is None or ldap_port is None else 
'{}:{}'.format(ldap_host,ldap_port)
+
+  def is_use_ssl(self):
+    '''
+    :return: Whether to use LDAP over SSL (LDAPS).
+    '''
+    return "true" == _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.connectivity.use_ssl')
+
+  def is_anonymous_bind(self):
+    '''
+    :return: Whether LDAP requests can connect anonymously or if a managed 
user is required to connect
+    '''
+    return "true" == _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.connectivity.anonymous_bind')
+
+  def get_bind_dn(self):
+    '''
+    :return: The DN of the manager account to use when binding to LDAP if 
anonymous binding is disabled or None if ldap-configuration/ is not specified
+    '''
+    return _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.connectivity.bind_dn')
+
+  def get_bind_password(self):
+    '''
+    :return: The password for the manager account used to bind to LDAP if 
anonymous binding is disabled
+    '''
+    return _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.connectivity.bind_password')
+
+  def get_dn_attribute(self):
+    '''
+    :return: The attribute used for determining what the distinguished name 
property is or None if ldap-configuration/ambari.ldap.attributes.dn_attr is not 
specified
+    '''
+    return _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.attributes.dn_attr')
+
+  def get_user_object_class(self):
+    '''
+    :return: The class to which user objects in LDAP belong or None if 
ldap-configuration/ambari.ldap.attributes.user.object_class is not specified
+    '''
+    return _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.attributes.user.object_class')
+
+  def get_user_name_attribute(self):
+    '''
+    :return: The attribute used for determining the user name, such as 'uid' 
or None if ldap-configuration/ambari.ldap.attributes.user.name_attr is not 
specified
+    '''
+    return _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.attributes.user.name_attr')
+
+  def get_user_search_base(self):
+    '''
+    :return: The base DN to use when filtering LDAP users and groups. This is 
only used when LDAP authentication is enabled or None if 
ldap-configuration/ambari.ldap.attributes.user.search_base is not specified
+    '''
+    return _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.attributes.user.search_base')
+
+  def get_group_object_class(self):
+    '''
+    :return: The LDAP object class value that defines groups in the directory 
service or None if ldap-configuration/ambari.ldap.attributes.group.object_class 
is not specified
+    '''
+    return _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.attributes.group.object_class')
+
+  def get_group_name_attribute(self):
+    '''
+    :return: The attribute used to determine the group name in LDAP  or None 
if ldap-configuration/ambari.ldap.attributes.group.name_attr is not specified
+    '''
+    return _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.attributes.group.name_attr')
+
+  def get_group_member_attribute(self):
+    '''
+    :return: The LDAP attribute which identifies group membership  or None if 
ldap-configuration/ambari.ldap.attributes.group.member_attr is not specified
+    '''
+    return _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.attributes.group.member_attr')
+
+  def get_group_search_base(self):
+    '''
+    :return: The base DN to use when filtering LDAP users and groups or None 
if ldap-configuration/ambari.ldap.attributes.group.search_base is not specified
+    '''
+    return _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.attributes.group.search_base')
+
+  def get_group_mapping_rules(self):
+    '''
+    :return: A comma-separate list of groups which would give a user 
administrative access to Ambari when syncing from LDAP or None if 
ldap-configuration/ambari.ldap.advanced.group_mapping_rules is not specified
+    '''
+    return _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.advanced.group_mapping_rules')
+
+  def get_user_search_filter(self):
+    '''
+    :return: A filter used to lookup a user in LDAP based on the Ambari user 
name or None if ldap-configuration/ambari.ldap.advanced.user_search_filter is 
not specified
+    '''
+    return _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.advanced.user_search_filter')
+
+  def get_user_member_replace_pattern(self):
+    '''
+    :return: The regex pattern to use when replacing the user member attribute 
ID value with a placeholder or None if 
ldap-configuration/ambari.ldap.advanced.user_member_replace_pattern is not 
specified
+    '''
+    return _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.advanced.user_member_replace_pattern')
+
+  def get_user_member_filter(self):
+    '''
+    :return: The filter to use for syncing user members of a group from LDAP 
or None if ldap-configuration/ambari.ldap.advanced.user_member_filter is not 
specified
+    '''
+    return _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.advanced.user_member_filter')
+
+  def get_group_search_filter(self):
+    '''
+    :return: The DN to use when searching for LDAP groups or None if 
ldap-configuration/ambari.ldap.advanced.group_search_filter is not specified
+    '''
+    return _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.advanced.group_search_filter')
+
+  def get_group_member_replace_pattern(self):
+    '''
+    :return: The regex pattern to use when replacing the group member 
attribute ID value with a placeholder or None if 
ldap-configuration/ambari.ldap.advanced.group_member_replace_pattern is not 
specified
+    '''
+    return _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.advanced.group_member_replace_pattern')
+
+  def get_group_member_filter(self):
+    '''
+    :return: The F=filter to use for syncing group members of a group from 
LDAP or None if ldap-configuration/ambari.ldap.advanced.group_member_filter is 
not specified
+    '''
+    return _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.advanced.group_member_filter')
+
+  def is_force_lower_case_user_names(self):
+    '''
+    :return: Whether to force the ldap user name to be lowercase or leave as-is
+    '''
+    return "true" == _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.advanced.force_lowercase_usernames')
+
+  def is_pagination_enabled(self):
+    '''
+    :return: Whether results from LDAP are paginated when requested
+    '''
+    return "true" == _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.advanced.pagination_enabled')
+
+  def is_follow_referral_handling(self):
+    '''
+    :return: Whether to follow LDAP referrals to other URLs when the LDAP 
controller doesn't have the requested object
+    '''
+    return "true" == _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.advanced.referrals')
+
+  def is_disable_endpoint_identification(self):
+    '''
+    :return: Whether to disable endpoint identification (hostname 
verification) during SSL handshake while updating from LDAP
+    '''
+    return "true" == _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.advanced.disable_endpoint_identification')
+
+  def is_ldap_alternate_user_search_enabled(self):
+    '''
+    :return: Whether a secondary (alternate) LDAP user search filer is used if 
the primary filter fails to find a user
+    '''
+    return "true" == _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.advanced.alternate_user_search_enabled')
+
+  def get_alternate_user_search_filter(self):
+    '''
+    :return: An alternate LDAP user search filter which can be used if 
'ldap_alternate_user_search_enabled' is enabled and the primary filter fails to 
find a user
+    '''
+    return _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.advanced.alternate_user_search_filter')
+
+  def get_sync_collision_handling_behavior(self):
+    '''
+    :return: How to handle username collision while updating from LDAP or None 
if ldap-configuration/ambari.ldap.advanced.collision_behavior is not specified
+    '''
+    return _get_from_dictionary(self.ldap_properties, 
'ambari.ldap.advanced.collision_behavior')
diff --git a/ambari-server/src/test/python/stacks/test_ambari_configuration.py 
b/ambari-server/src/test/python/TestAmbariConfiguration.py
similarity index 61%
rename from ambari-server/src/test/python/stacks/test_ambari_configuration.py
rename to ambari-server/src/test/python/TestAmbariConfiguration.py
index 5894e49..58062e1 100644
--- a/ambari-server/src/test/python/stacks/test_ambari_configuration.py
+++ b/ambari-server/src/test/python/TestAmbariConfiguration.py
@@ -26,7 +26,7 @@ class TestAmbariConfiguration(TestCase):
     import imp
     self.test_directory = os.path.dirname(os.path.abspath(__file__))
 
-    relative_path = '../../../main/resources/stacks/ambari_configuration.py'
+    relative_path = '../../main/resources/stacks/ambari_configuration.py'
     ambari_configuration_path = 
os.path.abspath(os.path.join(self.test_directory, relative_path))
     class_name = 'AmbariConfiguration'
 
@@ -41,6 +41,7 @@ class TestAmbariConfiguration(TestCase):
     ambari_configuration = self.ambari_configuration_class('{}')
     self.assertIsNone(ambari_configuration.get_ambari_server_configuration())
     self.assertIsNone(ambari_configuration.get_ambari_sso_configuration())
+    self.assertIsNone(ambari_configuration.get_ambari_ldap_configuration())
 
   def testMissingSSOConfiguration(self):
     services_json = {
@@ -280,3 +281,126 @@ class TestAmbariConfiguration(TestCase):
                       
'................................................................'
                       
'dXRpbmcxFzAVBgNVBAMTDmNsb3VkYnJlYWstcmdsMSUwIwYJKoZIhvcNAQkBFhZy',
                       ambari_sso_details.get_sso_provider_certificate(False, 
True))
+
+  def testMissingLDAPConfiguration(self):
+    services_json = {
+      "ambari-server-configuration": {
+      }
+    }
+
+    ambari_configuration = self.ambari_configuration_class(services_json)
+    self.assertIsNone(ambari_configuration.get_ambari_ldap_configuration())
+
+    ambari_ldap_details = ambari_configuration.get_ambari_ldap_details()
+    self.assertIsNotNone(ambari_ldap_details)
+    self.assertFalse(ambari_ldap_details.is_ldap_enabled())
+    self.assertIsNone(ambari_ldap_details.get_server_host())
+    self.assertIsNone(ambari_ldap_details.get_server_port())
+    self.assertIsNone(ambari_ldap_details.get_server_url())
+    self.assertIsNone(ambari_ldap_details.get_secondary_server_host())
+    self.assertIsNone(ambari_ldap_details.get_secondary_server_port())
+    self.assertIsNone(ambari_ldap_details.get_secondary_server_url())
+    self.assertFalse(ambari_ldap_details.is_use_ssl())
+    self.assertFalse(ambari_ldap_details.is_anonymous_bind())
+    self.assertIsNone(ambari_ldap_details.get_bind_dn())
+    self.assertIsNone(ambari_ldap_details.get_bind_password())
+    self.assertIsNone(ambari_ldap_details.get_dn_attribute())
+    self.assertIsNone(ambari_ldap_details.get_user_object_class())
+    self.assertIsNone(ambari_ldap_details.get_user_name_attribute())
+    self.assertIsNone(ambari_ldap_details.get_user_search_base())
+    self.assertIsNone(ambari_ldap_details.get_group_object_class())
+    self.assertIsNone(ambari_ldap_details.get_group_name_attribute())
+    self.assertIsNone(ambari_ldap_details.get_group_member_attribute())
+    self.assertIsNone(ambari_ldap_details.get_group_search_base())
+    self.assertIsNone(ambari_ldap_details.get_group_mapping_rules())
+    self.assertIsNone(ambari_ldap_details.get_user_search_filter())
+    self.assertIsNone(ambari_ldap_details.get_user_member_replace_pattern())
+    self.assertIsNone(ambari_ldap_details.get_user_member_filter())
+    self.assertIsNone(ambari_ldap_details.get_group_search_filter())
+    self.assertIsNone(ambari_ldap_details.get_group_member_replace_pattern())
+    self.assertIsNone(ambari_ldap_details.get_group_member_filter())
+    self.assertFalse(ambari_ldap_details.is_force_lower_case_user_names())
+    self.assertFalse(ambari_ldap_details.is_pagination_enabled())
+    self.assertFalse(ambari_ldap_details.is_follow_referral_handling())
+    self.assertFalse(ambari_ldap_details.is_disable_endpoint_identification())
+    
self.assertFalse(ambari_ldap_details.is_ldap_alternate_user_search_enabled())
+    self.assertIsNone(ambari_ldap_details.get_alternate_user_search_filter())
+    
self.assertIsNone(ambari_ldap_details.get_sync_collision_handling_behavior())
+
+  def testNotEmtpyLDAPConfiguration(self):
+    services_json = {
+        "ambari-server-configuration": {
+          "ldap-configuration": {
+            "ambari.ldap.authentication.enabled" : "true",
+            "ambari.ldap.connectivity.server.host" : "host1",
+            "ambari.ldap.connectivity.server.port" : "336",
+            "ambari.ldap.connectivity.secondary.server.host" : "host2",
+            "ambari.ldap.connectivity.secondary.server.port" : "337",
+            "ambari.ldap.connectivity.use_ssl" : "true",
+            "ambari.ldap.connectivity.anonymous_bind" : "true",
+            "ambari.ldap.connectivity.bind_dn" : "bind_dn",
+            "ambari.ldap.connectivity.bind_password" : "bind_password",
+            "ambari.ldap.attributes.dn_attr" : "dn_attr",
+            "ambari.ldap.attributes.user.object_class" : "user.object_class",
+            "ambari.ldap.attributes.user.name_attr" : "user.name_attr",
+            "ambari.ldap.attributes.user.search_base" : "user.search_base",
+            "ambari.ldap.attributes.group.object_class" : "group.object_class",
+            "ambari.ldap.attributes.group.name_attr" : "group.name_attr",
+            "ambari.ldap.attributes.group.member_attr" : "group.member_attr",
+            "ambari.ldap.attributes.group.search_base" : "group.search_base",
+            "ambari.ldap.advanced.group_mapping_rules" : "group_mapping_rules",
+            "ambari.ldap.advanced.user_search_filter" : "user_search_filter",
+            "ambari.ldap.advanced.user_member_replace_pattern" : 
"user_member_replace_pattern",
+            "ambari.ldap.advanced.user_member_filter" : "user_member_filter",
+            "ambari.ldap.advanced.group_search_filter" : "group_search_filter",
+            "ambari.ldap.advanced.group_member_replace_pattern" : 
"group_member_replace_pattern",
+            "ambari.ldap.advanced.group_member_filter" : "group_member_filter",
+            "ambari.ldap.advanced.force_lowercase_usernames" : "true",
+            "ambari.ldap.advanced.pagination_enabled" : "true",
+            "ambari.ldap.advanced.referrals" : "true",
+            "ambari.ldap.advanced.disable_endpoint_identification" : "true",
+            "ambari.ldap.advanced.alternate_user_search_enabled" : "true",
+            "ambari.ldap.advanced.alternate_user_search_filter" : 
"alternate_user_search_filter",
+            "ambari.ldap.advanced.collision_behavior" : "collision_behavior"
+        }
+      }
+    }
+
+    ambari_configuration = self.ambari_configuration_class(services_json)
+    self.assertIsNotNone(ambari_configuration.get_ambari_ldap_configuration())
+    ambari_ldap_details = ambari_configuration.get_ambari_ldap_details()
+    self.assertIsNotNone(ambari_ldap_details)
+
+    self.assertTrue(ambari_ldap_details.is_ldap_enabled())
+    self.assertEquals(ambari_ldap_details.get_server_host(), "host1")
+    self.assertEquals(ambari_ldap_details.get_server_port(), 336)
+    self.assertEquals(ambari_ldap_details.get_server_url(), "host1:336")
+    self.assertEquals(ambari_ldap_details.get_secondary_server_host(), "host2")
+    self.assertEquals(ambari_ldap_details.get_secondary_server_port(), 337)
+    self.assertEquals(ambari_ldap_details.get_secondary_server_url(), 
"host2:337")
+    self.assertTrue(ambari_ldap_details.is_use_ssl())
+    self.assertTrue(ambari_ldap_details.is_anonymous_bind())
+    self.assertEquals(ambari_ldap_details.get_bind_dn(), "bind_dn")
+    self.assertEquals(ambari_ldap_details.get_bind_password(), "bind_password")
+    self.assertEquals(ambari_ldap_details.get_dn_attribute(), "dn_attr")
+    self.assertEquals(ambari_ldap_details.get_user_object_class(), 
"user.object_class")
+    self.assertEquals(ambari_ldap_details.get_user_name_attribute(), 
"user.name_attr")
+    self.assertEquals(ambari_ldap_details.get_user_search_base(), 
"user.search_base")
+    self.assertEquals(ambari_ldap_details.get_group_object_class(), 
"group.object_class")
+    self.assertEquals(ambari_ldap_details.get_group_name_attribute(), 
"group.name_attr")
+    self.assertEquals(ambari_ldap_details.get_group_member_attribute(), 
"group.member_attr")
+    self.assertEquals(ambari_ldap_details.get_group_search_base(), 
"group.search_base")
+    self.assertEquals(ambari_ldap_details.get_group_mapping_rules(), 
"group_mapping_rules")
+    self.assertEquals(ambari_ldap_details.get_user_search_filter(), 
"user_search_filter")
+    self.assertEquals(ambari_ldap_details.get_user_member_replace_pattern(), 
"user_member_replace_pattern")
+    self.assertEquals(ambari_ldap_details.get_user_member_filter(), 
"user_member_filter")
+    self.assertEquals(ambari_ldap_details.get_group_search_filter(), 
"group_search_filter")
+    self.assertEquals(ambari_ldap_details.get_group_member_replace_pattern(), 
"group_member_replace_pattern")
+    self.assertEquals(ambari_ldap_details.get_group_member_filter(), 
"group_member_filter")
+    self.assertTrue(ambari_ldap_details.is_force_lower_case_user_names())
+    self.assertTrue(ambari_ldap_details.is_pagination_enabled())
+    self.assertTrue(ambari_ldap_details.is_follow_referral_handling())
+    self.assertTrue(ambari_ldap_details.is_disable_endpoint_identification())
+    
self.assertTrue(ambari_ldap_details.is_ldap_alternate_user_search_enabled())
+    self.assertEquals(ambari_ldap_details.get_alternate_user_search_filter(), 
"alternate_user_search_filter")
+    
self.assertEquals(ambari_ldap_details.get_sync_collision_handling_behavior(), 
"collision_behavior")

Reply via email to