Cleaned up content of MsoLogger
[so.git] / bpmn / MSOCommonBPMN / src / main / groovy / org / onap / so / bpmn / common / scripts / VnfAdapterRestV1.groovy
1 /*-
2  * ============LICENSE_START=======================================================
3  * ONAP - SO
4  * ================================================================================
5  * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
6  * ================================================================================
7  * Modifications Copyright (c) 2019 Samsung
8  * ================================================================================
9  * Licensed under the Apache License, Version 2.0 (the "License");
10  * you may not use this file except in compliance with the License.
11  * You may obtain a copy of the License at
12  *
13  *      http://www.apache.org/licenses/LICENSE-2.0
14  *
15  * Unless required by applicable law or agreed to in writing, software
16  * distributed under the License is distributed on an "AS IS" BASIS,
17  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18  * See the License for the specific language governing permissions and
19  * limitations under the License.
20  * ============LICENSE_END=========================================================
21  */
22
23 package org.onap.so.bpmn.common.scripts
24
25 import org.onap.so.client.HttpClientFactory
26
27 import javax.ws.rs.core.Response
28 import org.apache.commons.lang3.*
29 import org.camunda.bpm.engine.delegate.BpmnError
30 import org.camunda.bpm.engine.delegate.DelegateExecution
31 import org.onap.so.bpmn.core.UrnPropertiesReader
32 import org.onap.so.client.HttpClient
33 import org.onap.so.logger.MessageEnum
34 import org.onap.so.logger.MsoLogger
35 import org.slf4j.Logger
36 import org.slf4j.LoggerFactory
37 import org.onap.so.utils.TargetEntity
38 import java.util.UUID
39
40
41
42
43 class VnfAdapterRestV1 extends AbstractServiceTaskProcessor {
44     private static final Logger logger = LoggerFactory.getLogger( VnfAdapterRestV1.class);
45
46
47         ExceptionUtil exceptionUtil = new ExceptionUtil()
48
49         // VNF Response Processing
50         public void preProcessRequest (DelegateExecution execution) {
51                 def method = getClass().getSimpleName() + '.preProcessRequest(' +
52                         'execution=' + execution.getId() +
53                         ')'
54                 logger.trace('Entered ' + method)
55
56                 def prefix="VNFREST_"
57                 execution.setVariable("prefix", prefix)
58                 setSuccessIndicator(execution, false)
59
60                 try {
61                         String request = validateRequest(execution, "mso-request-id")
62
63                         // Get the request type (the name of the root element) from the request
64
65                         Node root = new XmlParser().parseText(request)
66                         String requestType = root.name()
67                         execution.setVariable(prefix + 'requestType', requestType)
68                         logger.debug(getProcessKey(execution) + ': ' + prefix + 'requestType = ' + requestType)
69
70                         logger.debug('VnfAdapterRestV1, request: ' + request)
71                         // Get the messageId from the request
72
73                         String messageId = getChildText(root, 'messageId')
74
75                         if ('rollbackVolumeGroupRequest'.equals(requestType)) {
76                                 messageId = getMessageIdForVolumeGroupRollback(root)
77                         }
78
79                         if (messageId == null || messageId.isEmpty()) {
80                                 String msg = getProcessKey(execution) + ': no messageId in ' + requestType
81                                 logger.error("{} {} {} {}", MessageEnum.BPMN_GENERAL_EXCEPTION_ARG.toString(), msg, "BPMN",
82                                                 MsoLogger.ErrorCode.UnknownError.getValue());
83                                 exceptionUtil.buildAndThrowWorkflowException(execution, 2000, msg)
84                         }
85
86                         execution.setVariable('VNFAResponse_CORRELATOR', messageId)
87                         logger.debug(getProcessKey(execution) + ': VNFAResponse_CORRELATOR = ' + messageId)
88
89                         // Get the notificationUrl from the request
90
91                         String notificationUrl = getChildText(root, 'notificationUrl')
92
93                         if (notificationUrl == null || notificationUrl.isEmpty()) {
94                                 String msg = getProcessKey(execution) + ': no notificationUrl in ' + requestType
95                                 logger.error("{} {} {} {}", MessageEnum.BPMN_GENERAL_EXCEPTION_ARG.toString(), msg, "BPMN",
96                                                 MsoLogger.ErrorCode.UnknownError.getValue());
97                                 exceptionUtil.buildAndThrowWorkflowException(execution, 2000, msg)
98                         }
99
100                         execution.setVariable(prefix + 'notificationUrl', notificationUrl)
101                         logger.debug(getProcessKey(execution) + ': ' + prefix + 'notificationUrl = ' + notificationUrl)
102
103                         // Determine the VnfAdapter endpoint
104
105                         String vnfAdapterEndpoint = UrnPropertiesReader.getVariable("mso.adapters.vnf.rest.endpoint", execution)
106
107                         if (vnfAdapterEndpoint == null || vnfAdapterEndpoint.isEmpty()) {
108                                 String msg = getProcessKey(execution) + ': mso:adapters:vnf:rest:endpoint URN mapping is not defined'
109                                 logger.error("{} {} {} {}", MessageEnum.BPMN_GENERAL_EXCEPTION_ARG.toString(), msg, "BPMN",
110                                                 MsoLogger.ErrorCode.UnknownError.getValue());
111                                 exceptionUtil.buildAndThrowWorkflowException(execution, 2000, msg)
112                         }
113
114                         while (vnfAdapterEndpoint.endsWith('/')) {
115                                 vnfAdapterEndpoint = vnfAdapterEndpoint.substring(0, vnfAdapterEndpoint.length()-1)
116                         }
117
118                         String vnfAdapterMethod = null
119                         String vnfAdapterUrl = null
120                         String vnfAdapterRequest = request
121
122                         if ('createVfModuleRequest'.equals(requestType)) {
123                                 String vnfId = getChildText(root, 'vnfId')
124
125                                 if (vnfId == null || vnfId.isEmpty()) {
126                                         String msg = getProcessKey(execution) + ': no vnfId in ' + requestType
127                                         logger.error("{} {} {} {}", MessageEnum.BPMN_GENERAL_EXCEPTION_ARG.toString(), msg, "BPMN",
128                                                         MsoLogger.ErrorCode.UnknownError.getValue());
129                                         exceptionUtil.buildAndThrowWorkflowException(execution, 2000, msg)
130                                 }
131
132                                 vnfAdapterMethod = 'POST'
133                                 vnfAdapterUrl = vnfAdapterEndpoint + '/' + URLEncoder.encode(vnfId, 'UTF-8') + '/vf-modules'
134
135                         } else if ('updateVfModuleRequest'.equals(requestType)) {
136                                 String vnfId = getChildText(root, 'vnfId')
137
138                                 if (vnfId == null || vnfId.isEmpty()) {
139                                         String msg = getProcessKey(execution) + ': no vnfId in ' + requestType
140                                         logger.error("{} {} {} {}", MessageEnum.BPMN_GENERAL_EXCEPTION_ARG.toString(), msg, "BPMN",
141                                                         MsoLogger.ErrorCode.UnknownError.getValue());
142                                         exceptionUtil.buildAndThrowWorkflowException(execution, 2000, msg)
143                                 }
144
145                                 String vfModuleId = getChildText(root, 'vfModuleId')
146
147                                 if (vfModuleId == null || vfModuleId.isEmpty()) {
148                                         String msg = getProcessKey(execution) + ': no vfModuleId in ' + requestType
149                                         logger.error("{} {} {} {}", MessageEnum.BPMN_GENERAL_EXCEPTION_ARG.toString(), msg, "BPMN",
150                                                         MsoLogger.ErrorCode.UnknownError.getValue());
151                                         exceptionUtil.buildAndThrowWorkflowException(execution, 2000, msg)
152                                 }
153
154                                 vnfAdapterMethod = 'PUT'
155                                 vnfAdapterUrl = vnfAdapterEndpoint + '/' + URLEncoder.encode(vnfId, 'UTF-8') +
156                                         '/vf-modules/' + URLEncoder.encode(vfModuleId, 'UTF-8')
157
158                         } else if ('deleteVfModuleRequest'.equals(requestType)) {
159                                 String vnfId = getChildText(root, 'vnfId')
160
161                                 if (vnfId == null || vnfId.isEmpty()) {
162                                         String msg = getProcessKey(execution) + ': no vnfId in ' + requestType
163                                         logger.error("{} {} {} {}", MessageEnum.BPMN_GENERAL_EXCEPTION_ARG.toString(), msg, "BPMN",
164                                                         MsoLogger.ErrorCode.UnknownError.getValue());
165                                         exceptionUtil.buildAndThrowWorkflowException(execution, 2000, msg)
166                                 }
167
168                                 String vfModuleId = getChildText(root, 'vfModuleId')
169
170                                 if (vfModuleId == null || vfModuleId.isEmpty()) {
171                                         String msg = getProcessKey(execution) + ': no vfModuleId in ' + requestType
172                                         logger.error("{} {} {} {}", MessageEnum.BPMN_GENERAL_EXCEPTION_ARG.toString(), msg, "BPMN",
173                                                         MsoLogger.ErrorCode.UnknownError.getValue());
174                                         exceptionUtil.buildAndThrowWorkflowException(execution, 2000, msg)
175                                 }
176
177                                 vnfAdapterMethod = 'DELETE'
178                                 vnfAdapterUrl = vnfAdapterEndpoint + '/' + URLEncoder.encode(vnfId, 'UTF-8') +
179                                         '/vf-modules/' + URLEncoder.encode(vfModuleId, 'UTF-8')
180
181                         } else if ('rollbackVfModuleRequest'.equals(requestType)) {
182                                 Node vfModuleRollbackNode = getChild(root, 'vfModuleRollback')
183
184                                 if (vfModuleRollbackNode == null) {
185                                         String msg = getProcessKey(execution) + ': no vfModuleRollback in ' + requestType
186                                         logger.error("{} {} {} {}", MessageEnum.BPMN_GENERAL_EXCEPTION_ARG.toString(), msg, "BPMN",
187                                                         MsoLogger.ErrorCode.UnknownError.getValue());
188                                         exceptionUtil.buildAndThrowWorkflowException(execution, 2000, msg)
189                                 }
190
191                                 String vnfId = getChildText(vfModuleRollbackNode, 'vnfId')
192
193                                 if (vnfId == null || vnfId.isEmpty()) {
194                                         String msg = getProcessKey(execution) + ': no vnfId in ' + requestType
195                                         logger.error("{} {} {} {}", MessageEnum.BPMN_GENERAL_EXCEPTION_ARG.toString(), msg, "BPMN",
196                                                         MsoLogger.ErrorCode.UnknownError.getValue());
197                                         exceptionUtil.buildAndThrowWorkflowException(execution, 2000, msg)
198                                 }
199
200                                 String vfModuleId = getChildText(vfModuleRollbackNode, 'vfModuleId')
201
202                                 if (vfModuleId == null || vfModuleId.isEmpty()) {
203                                         String msg = getProcessKey(execution) + ': no vfModuleId in ' + requestType
204                                         logger.error("{} {} {} {}", MessageEnum.BPMN_GENERAL_EXCEPTION_ARG.toString(), msg, "BPMN",
205                                                         MsoLogger.ErrorCode.UnknownError.getValue());
206                                         exceptionUtil.buildAndThrowWorkflowException(execution, 2000, msg)
207                                 }
208
209                                 vnfAdapterMethod = 'DELETE'
210                                 vnfAdapterUrl = vnfAdapterEndpoint + '/' + URLEncoder.encode(vnfId, 'UTF-8') +
211                                         '/vf-modules/' + URLEncoder.encode(vfModuleId, 'UTF-8') + '/rollback'
212
213                         } else if ('createVolumeGroupRequest'.equals(requestType)) {
214                                 vnfAdapterMethod = 'POST'
215                                 if (vnfAdapterEndpoint.endsWith('v1/vnfs')) {
216                                         vnfAdapterEndpoint = vnfAdapterEndpoint.substring(0, (vnfAdapterEndpoint.length()-'/vnfs'.length()))
217                                 }
218                                 vnfAdapterUrl = vnfAdapterEndpoint + '/volume-groups'
219
220                         } else if ('updateVolumeGroupRequest'.equals(requestType)) {
221                                 String volumeGroupId = getChildText(root, 'volumeGroupId')
222
223                                 if (volumeGroupId == null || volumeGroupId.isEmpty()) {
224                                         String msg = getProcessKey(execution) + ': no volumeGroupId in ' + requestType
225                                         logger.error("{} {} {} {}", MessageEnum.BPMN_GENERAL_EXCEPTION_ARG.toString(), msg, "BPMN",
226                                                         MsoLogger.ErrorCode.UnknownError.getValue());
227                                         exceptionUtil.buildAndThrowWorkflowException(execution, 2000, msg)
228                                 }
229
230                                 vnfAdapterMethod = 'PUT'
231                                 if (vnfAdapterEndpoint.endsWith('v1/vnfs')) {
232                                         vnfAdapterEndpoint = vnfAdapterEndpoint.substring(0, (vnfAdapterEndpoint.length()-'/vnfs'.length()))
233                                 }
234                                 vnfAdapterUrl = vnfAdapterEndpoint + '/volume-groups/' + URLEncoder.encode(volumeGroupId, 'UTF-8')
235
236                         } else if ('deleteVolumeGroupRequest'.equals(requestType)) {
237                                 String volumeGroupId = getChildText(root, 'volumeGroupId')
238
239                                 if (volumeGroupId == null || volumeGroupId.isEmpty()) {
240                                         String msg = getProcessKey(execution) + ': no volumeGroupId in ' + requestType
241                                         logger.error("{} {} {} {}", MessageEnum.BPMN_GENERAL_EXCEPTION_ARG.toString(), msg, "BPMN",
242                                                         MsoLogger.ErrorCode.UnknownError.getValue());
243                                         exceptionUtil.buildAndThrowWorkflowException(execution, 2000, msg)
244                                 }
245
246                                 vnfAdapterMethod = 'DELETE'
247                                 if (vnfAdapterEndpoint.endsWith('v1/vnfs')) {
248                                         vnfAdapterEndpoint = vnfAdapterEndpoint.substring(0, (vnfAdapterEndpoint.length()-'/vnfs'.length()))
249                                 }
250                                 vnfAdapterUrl = vnfAdapterEndpoint + '/volume-groups/' + URLEncoder.encode(volumeGroupId, 'UTF-8')
251
252                         } else if ('rollbackVolumeGroupRequest'.equals(requestType)) {
253                                 String volumeGroupId = getVolumeGroupIdFromRollbackRequest(root)
254
255                                 if (volumeGroupId == null || volumeGroupId.isEmpty()) {
256                                         String msg = getProcessKey(execution) + ': no volumeGroupId in ' + requestType
257                                         logger.error("{} {} {} {}", MessageEnum.BPMN_GENERAL_EXCEPTION_ARG.toString(), msg, "BPMN",
258                                                         MsoLogger.ErrorCode.UnknownError.getValue());
259                                         exceptionUtil.buildAndThrowWorkflowException(execution, 2000, msg)
260                                 }
261
262                                 vnfAdapterMethod = 'DELETE'
263                                 if (vnfAdapterEndpoint.endsWith('v1/vnfs')) {
264                                         vnfAdapterEndpoint = vnfAdapterEndpoint.substring(0, (vnfAdapterEndpoint.length()-'/vnfs'.length()))
265                                 }
266                                 vnfAdapterUrl = vnfAdapterEndpoint + '/volume-groups/' + URLEncoder.encode(volumeGroupId, 'UTF-8')  + '/rollback'
267
268                         } else {
269                                 String msg = getProcessKey(execution) + ': Unsupported request type: ' + requestType
270                                 logger.error("{} {} {} {}", MessageEnum.BPMN_GENERAL_EXCEPTION_ARG.toString(), msg, "BPMN",
271                                                 MsoLogger.ErrorCode.UnknownError.getValue());
272                                 exceptionUtil.buildAndThrowWorkflowException(execution, 2000, msg)
273                         }
274
275                         execution.setVariable(prefix + 'vnfAdapterMethod', vnfAdapterMethod)
276                         logger.debug(getProcessKey(execution) + ': ' + prefix + 'vnfAdapterMethod = ' + vnfAdapterMethod)
277                         execution.setVariable(prefix + 'vnfAdapterUrl', vnfAdapterUrl)
278                         logger.debug(getProcessKey(execution) + ': ' + prefix + 'vnfAdapterUrl = ' + vnfAdapterUrl)
279                         execution.setVariable(prefix + 'vnfAdapterRequest', vnfAdapterRequest)
280                         logger.debug(getProcessKey(execution) + ': ' + prefix + 'vnfAdapterRequest = \n' + vnfAdapterRequest)
281
282                         // Get the Basic Auth credentials for the VnfAdapter
283
284                         String basicAuthValue = UrnPropertiesReader.getVariable("mso.adapters.po.auth", execution)
285
286                         if (basicAuthValue == null || basicAuthValue.isEmpty()) {
287                                 logger.error("{} {} {} {}", MessageEnum.BPMN_GENERAL_EXCEPTION_ARG.toString(),
288                                                 getProcessKey(execution) + ": mso:adapters:po:auth URN mapping is not defined", "BPMN",
289                                                 MsoLogger.ErrorCode.UnknownError.getValue());
290                         } else {
291                                 try {
292                                         def encodedString = utils.getBasicAuth(basicAuthValue, UrnPropertiesReader.getVariable("mso.msoKey", execution))
293                                         execution.setVariable(prefix + 'basicAuthHeaderValue', encodedString)
294                                 } catch (IOException ex) {
295                                         logger.error("{} {} {} {}", MessageEnum.BPMN_GENERAL_EXCEPTION_ARG.toString(),
296                                                         getProcessKey(execution) + ": Unable to encode BasicAuth credentials for VnfAdapter",
297                                                         "BPMN", MsoLogger.ErrorCode.UnknownError.getValue());
298                                 }
299                         }
300
301                 } catch (BpmnError e) {
302                         logger.debug(" Rethrowing MSOWorkflowException")
303                         throw e
304                 } catch (Exception e) {
305                         String msg = 'Caught exception in ' + method + ": " + e
306                         logger.error("{} {} {} {}", MessageEnum.BPMN_GENERAL_EXCEPTION_ARG.toString(), msg, "BPMN",
307                                         MsoLogger.ErrorCode.UnknownError.getValue());
308                         logger.debug(msg)
309                         exceptionUtil.buildAndThrowWorkflowException(execution, 2000, msg)
310                 }
311         }
312
313         public String getVolumeGroupIdFromRollbackRequest(Node root) {
314                 return root.'volumeGroupRollback'.'volumeGroupId'.text()
315         }
316
317         public String getMessageIdForVolumeGroupRollback(Node root) {
318                 return root.'volumeGroupRollback'.'messageId'.text()
319         }
320
321         /**
322          * This method is used instead of an HTTP Connector task because the
323          * connector does not allow DELETE with a body.
324          */
325         public void sendRequestToVnfAdapter(DelegateExecution execution) {
326                 def method = getClass().getSimpleName() + '.sendRequestToVnfAdapter(' +
327                         'execution=' + execution.getId() +
328                         ')'
329                 logger.trace('Entered ' + method)
330
331                 String prefix = execution.getVariable('prefix')
332
333                 try {
334                         String vnfAdapterMethod = execution.getVariable(prefix + 'vnfAdapterMethod')
335                         String vnfAdapterUrl = execution.getVariable(prefix + 'vnfAdapterUrl')
336                         String vnfAdapterRequest = execution.getVariable(prefix + 'vnfAdapterRequest')
337
338                         URL url = new URL(vnfAdapterUrl);
339
340                         HttpClient httpClient = new HttpClientFactory().newXmlClient(url, TargetEntity.VNF_ADAPTER)
341                         httpClient.addAdditionalHeader("Authorization", execution.getVariable(prefix + "basicAuthHeaderValue"))
342                         
343                         httpClient.addAdditionalHeader("X-ONAP-RequestID", execution.getVariable("mso-request-id"))
344                         httpClient.addAdditionalHeader("X-ONAP-InvocationID", UUID.randomUUID().toString())
345                         httpClient.addAdditionalHeader("X-ONAP-PartnerName", "SO-VNFAdapter")
346                         Response response;
347
348                         if ("GET".equals(vnfAdapterMethod)) {
349                                 response = httpClient.get()
350                         } else if ("PUT".equals(vnfAdapterMethod)) {
351                                 response = httpClient.put(vnfAdapterRequest)
352                         } else if ("POST".equals(vnfAdapterMethod)) {
353                                 response = httpClient.post(vnfAdapterRequest)
354                         } else if ("DELETE".equals(vnfAdapterMethod)) {
355                                 response = httpClient.delete(vnfAdapterRequest)
356                         } else {
357                                 String msg = 'Unsupported HTTP method "' + vnfAdapterMethod + '" in ' + method + ": " + e
358                                 logger.error("{} {} {} {}", MessageEnum.BPMN_GENERAL_EXCEPTION_ARG.toString(), msg, "BPMN",
359                                                 MsoLogger.ErrorCode.UnknownError.getValue());
360                                 exceptionUtil.buildAndThrowWorkflowException(execution, 2000, msg)
361                         }
362
363                         execution.setVariable(prefix + "vnfAdapterStatusCode", response.getStatus())
364                         if(response.hasEntity()){
365                                 execution.setVariable(prefix + "vnfAdapterResponse", response.readEntity(String.class))
366                         }
367                 } catch (BpmnError e) {
368                         throw e
369                 } catch (Exception e) {
370                         String msg = 'Caught exception in ' + method + ": " + e
371                         logger.error("{} {} {} {}", MessageEnum.BPMN_GENERAL_EXCEPTION_ARG.toString(), msg, "BPMN",
372                                         MsoLogger.ErrorCode.UnknownError.getValue());
373                         exceptionUtil.buildAndThrowWorkflowException(execution, 2000, msg)
374                 }
375         }
376
377         public void processCallback(DelegateExecution execution){
378                 def method = getClass().getSimpleName() + '.processCallback(' +
379                         'execution=' + execution.getId() +
380                         ')'
381                 logger.trace('Entered ' + method)
382
383                 String callback = execution.getVariable('VNFAResponse_MESSAGE')
384
385                 try {
386                         logger.debug(getProcessKey(execution) + ": received callback:\n" + callback)
387
388                         // The XML callback is available to the calling flow in any case,
389                         // even if a WorkflowException is generated.
390                         execution.setVariable(getProcessKey(execution) + 'Response', callback)
391                         // TODO: Should deprecate use of processKey+Response variable for the response. Will use "WorkflowResponse" instead.
392                         execution.setVariable("WorkflowResponse", callback)
393
394                         callback = utils.removeXmlPreamble(callback)
395
396                         Node root = new XmlParser().parseText(callback)
397                         if (root.name().endsWith('Exception')) {
398                                 vnfAdapterWorkflowException(execution, callback)
399                         }
400                 } catch (Exception e) {
401                         logger.debug("Error encountered within VnfAdapterRest ProcessCallback method", e)
402                         exceptionUtil.buildAndThrowWorkflowException(execution, 7020, "Error encountered within VnfAdapterRest ProcessCallback method")
403                 }
404         }
405
406         /**
407          * Tries to parse the response as XML to extract the information to create
408          * a WorkflowException.  If the response cannot be parsed, a more generic
409          * WorkflowException is created.
410          */
411         public void vnfAdapterWorkflowException(DelegateExecution execution, Object response) {
412                 try {
413                         Node root = new XmlParser().parseText(response)
414                         String category = getChildText(root, "category")
415                         category = category == null || category.isEmpty() ? "" : " category='" + category + "'"
416                         String message = getChildText(root, "message")
417                         message = message == null || message.isEmpty() ? "" : " message='" + message + "'"
418                         String rolledBack = getChildText(root, "rolledBack")
419                         rolledBack = rolledBack == null || rolledBack.isEmpty() ? "" : " rolledBack='" + rolledBack + "'"
420                         exceptionUtil.buildWorkflowException(execution, 7020, "Received " + root.name() +
421                                 " from VnfAdapter:" + category + message + rolledBack);
422                 } catch (Exception e) {
423                         response = response == null || String.valueOf(response).isEmpty() ? "NONE" : response
424                         exceptionUtil.buildWorkflowException(execution, 7020, "Received error from VnfAdapter: " + response)
425                 }
426         }
427
428         /**
429          * Gets the named child of the specified node.
430          * @param node the node
431          * @param name the child name
432          * @return the child node, or null if no such child exists
433          */
434         private Node getChild(Node node, String name) {
435                 for (Node child : node.children()) {
436                         if (child.name() == name) {
437                                 return child
438                         }
439                 }
440                 return null
441         }
442
443         /**
444          * Gets the text of the named child of the specified node.
445          * @param node the node
446          * @param name the child name
447          * @return the child node text, or null if no such child exists
448          */
449         private String getChildText(Node node, String name) {
450                 Node child = getChild(node, name)
451                 return child == null ? null : child.text()
452         }
453 }