|
1 |
# passerelle - uniform access to multiple data sources and services
|
|
2 |
# Copyright (C) 2019 Entr'ouvert
|
|
3 |
#
|
|
4 |
# This program is free software: you can redistribute it and/or modify it
|
|
5 |
# under the terms of the GNU Affero General Public License as published
|
|
6 |
# by the Free Software Foundation, either version 3 of the License, or
|
|
7 |
# (at your option) any later version.
|
|
8 |
#
|
|
9 |
# This program is distributed in the hope that it will be useful,
|
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12 |
# GNU Affero General Public License for more details.
|
|
13 |
#
|
|
14 |
# You should have received a copy of the GNU Affero General Public License
|
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16 |
|
|
17 |
import collections
|
|
18 |
|
|
19 |
import zeep
|
|
20 |
import zeep.helpers
|
|
21 |
import zeep.xsd
|
|
22 |
from django.db import models
|
|
23 |
from django.utils.functional import cached_property
|
|
24 |
from django.utils.translation import ugettext_lazy as _
|
|
25 |
|
|
26 |
from passerelle.base.models import BaseResource, HTTPResource
|
|
27 |
from passerelle.utils.api import endpoint
|
|
28 |
from passerelle.utils.json import unflatten
|
|
29 |
|
|
30 |
|
|
31 |
class SOAPConnector(BaseResource, HTTPResource):
|
|
32 |
wsdl_url = models.URLField(
|
|
33 |
max_length=400, verbose_name=_('WSDL URL'), help_text=_('URL of the WSDL file')
|
|
34 |
)
|
|
35 |
zeep_strict = models.BooleanField(default=False, verbose_name=_('Be strict with returned XML'))
|
|
36 |
zeep_xsd_ignore_sequence_order = models.BooleanField(
|
|
37 |
default=True, verbose_name=_('Ignore sequence order')
|
|
38 |
)
|
|
39 |
category = _('Business Process Connectors')
|
|
40 |
|
|
41 |
class Meta:
|
|
42 |
verbose_name = _('SOAP connector')
|
|
43 |
|
|
44 |
@classmethod
|
|
45 |
def get_manager_form_class(cls, **kwargs):
|
|
46 |
form_class = super().get_manager_form_class(**kwargs)
|
|
47 |
fields = list(form_class.base_fields.items())
|
|
48 |
form_class.base_fields = collections.OrderedDict(fields[:2] + fields[-3:] + fields[2:-3])
|
|
49 |
return form_class
|
|
50 |
|
|
51 |
@cached_property
|
|
52 |
def client(self):
|
|
53 |
return self.soap_client(
|
|
54 |
wsdl_url=self.wsdl_url,
|
|
55 |
settings=zeep.Settings(
|
|
56 |
strict=self.zeep_strict, xsd_ignore_sequence_order=self.zeep_xsd_ignore_sequence_order
|
|
57 |
),
|
|
58 |
)
|
|
59 |
|
|
60 |
@endpoint(
|
|
61 |
methods=['get', 'post'],
|
|
62 |
perm='can_access',
|
|
63 |
name='method',
|
|
64 |
pattern=r'^(?P<method_name>\w+)/$',
|
|
65 |
example_pattern='method_name/',
|
|
66 |
description=_('Call a SOAP method'),
|
|
67 |
)
|
|
68 |
def method(self, request, method_name, post_data=None, **kwargs):
|
|
69 |
def jsonify(data):
|
|
70 |
if isinstance(data, (dict, collections.OrderedDict)):
|
|
71 |
# ignore _raw_elements, zeep put there nodes not maching the
|
|
72 |
# XSD when strict parsing is disabled.
|
|
73 |
return {
|
|
74 |
jsonify(k): jsonify(v)
|
|
75 |
for k, v in data.items()
|
|
76 |
if (self.zeep_strict or k != '_raw_elements')
|
|
77 |
}
|
|
78 |
elif isinstance(data, (list, tuple, collections.deque)):
|
|
79 |
return [jsonify(item) for item in data]
|
|
80 |
else:
|
|
81 |
return data
|
|
82 |
|
|
83 |
payload = {k: request.getlist(k) for k in request.GET if k != 'raise'}
|
|
84 |
payload.update(unflatten(post_data or {}))
|
|
85 |
soap_response = getattr(self.client.service, method_name)(**payload)
|
|
86 |
serialized = zeep.helpers.serialize_object(soap_response)
|
|
87 |
json_response = jsonify(serialized)
|
|
88 |
return {'err': 0, 'data': json_response}
|
|
89 |
|
|
90 |
def get_endpoints_infos(self):
|
|
91 |
endpoints = super().get_endpoints_infos()
|
|
92 |
for name, input_schema, output_schema in self.operations_and_schemas:
|
|
93 |
kwargs = dict(
|
|
94 |
name='method',
|
|
95 |
pattern=f'{name}/',
|
|
96 |
example_pattern=f'{name}/',
|
|
97 |
description=f'Method {name}',
|
|
98 |
json_schema_response={
|
|
99 |
'type': 'object',
|
|
100 |
'properties': collections.OrderedDict(
|
|
101 |
[
|
|
102 |
('err', {'type': 'integer'}),
|
|
103 |
('data', output_schema),
|
|
104 |
]
|
|
105 |
),
|
|
106 |
},
|
|
107 |
)
|
|
108 |
if input_schema:
|
|
109 |
kwargs['post_json_schema'] = input_schema
|
|
110 |
endpoints.append(endpoint(**kwargs))
|
|
111 |
endpoints[-1].object = self
|
|
112 |
endpoints[-1].func = self.method
|
|
113 |
if input_schema:
|
|
114 |
endpoints[-1].http_method = 'post'
|
|
115 |
else:
|
|
116 |
endpoints[-1].http_method = 'get'
|
|
117 |
return endpoints
|
|
118 |
|
|
119 |
@property
|
|
120 |
def operations_and_schemas(self):
|
|
121 |
operations = self.client.service._binding._operations
|
|
122 |
for name in operations:
|
|
123 |
operation = operations[name]
|
|
124 |
input_type = operation.input.body.type
|
|
125 |
output_type = operation.output.body.type
|
|
126 |
input_schema = self.type2schema(input_type, root=True)
|
|
127 |
output_schema = self.type2schema(output_type, root=True)
|
|
128 |
yield name, input_schema, output_schema
|
|
129 |
|
|
130 |
def type2schema(self, xsd_type, root=False):
|
|
131 |
# simplify schema: when a type contains a unique element, it will try
|
|
132 |
# to match any dict or list with it on input and will flatten the
|
|
133 |
# schema on output.
|
|
134 |
if isinstance(xsd_type, zeep.xsd.ComplexType) and len(xsd_type.elements) == 1 and not root:
|
|
135 |
if xsd_type.elements[0][1].max_occurs != 1:
|
|
136 |
return {'type': 'array', 'items': self.type2schema(xsd_type.elements[0][1].type)}
|
|
137 |
return self.type2schema(xsd_type.elements[0][1].type)
|
|
138 |
if isinstance(xsd_type, zeep.xsd.ComplexType):
|
|
139 |
properties = collections.OrderedDict()
|
|
140 |
schema = {
|
|
141 |
'type': 'object',
|
|
142 |
'properties': properties,
|
|
143 |
}
|
|
144 |
for key, element in xsd_type.elements:
|
|
145 |
if element.min_occurs > 0:
|
|
146 |
schema.setdefault('required', []).append(key)
|
|
147 |
element_schema = self.type2schema(element.type)
|
|
148 |
if element.max_occurs == 'unbounded' or element.max_occurs > 1:
|
|
149 |
element_schema = {'type': 'array', 'items': element_schema}
|
|
150 |
properties[key] = element_schema
|
|
151 |
if not properties:
|
|
152 |
return None
|
|
153 |
return schema
|
|
154 |
if isinstance(xsd_type, zeep.xsd.BuiltinType):
|
|
155 |
return {'type': 'string'}
|
|
156 |
return f'!!! UNKNOWN TYPE {xsd_type} !!!'
|